From a3feed10a4a0e12387110831dd2e530f3ad2ab6e Mon Sep 17 00:00:00 2001 From: username Date: Fri, 12 Jan 2024 21:21:26 +0800 Subject: [PATCH] Site updated: 2024-01-12 21:21:22 --- .../index.html" | 4 + .../index.html" | 4 + .../index.html" | 4 + .../index.html" | 4 + 2022/12/21/JavaWeb/index.html | 4 + 2023/01/10/xv6$chap1/index.html | 8 +- 2023/01/10/xv6$chap2/index.html | 8 +- 2023/01/10/xv6$chap3/index.html | 8 +- 2023/01/10/xv6$chap4/index.html | 8 +- 2023/01/10/xv6$chap5/index.html | 8 +- 2023/01/10/xv6$chap6/index.html | 8 +- 2023/01/10/xv6$chap7/index.html | 6 +- 2023/01/10/xv6$chap8/index.html | 4 + 2023/01/10/xv6$chap9/index.html | 4 + 2023/01/10/xv6/index.html | 4 + 2023/02/25/cs144$else/index.html | 8 +- 2023/02/25/cs144$lab0/index.html | 8 +- 2023/02/25/cs144$lab1/index.html | 8 +- 2023/02/25/cs144$lab2/index.html | 8 +- 2023/02/25/cs144$lab3/index.html | 8 +- 2023/02/25/cs144$lab4/index.html | 8 +- 2023/02/25/cs144$lab5/index.html | 8 +- 2023/02/25/cs144$lab6/index.html | 8 +- 2023/02/25/cs144/index.html | 8 +- .../index.html" | 8 +- 2023/03/13/cmu15445$lab0/index.html | 4 + 2023/03/13/cmu15445$lab1/index.html | 8 +- 2023/03/13/cmu15445$lab2/index.html | 6 +- 2023/03/13/cmu15445/index.html | 8 +- .../index.html" | 4 + 2023/06/21/comporgan/index.html | 4 + .../index.html" | 4 + 2023/08/12/kernel_compile/index.html | 4 + 2023/08/27/2023-os-comp/index.html | 8 + .../index.html" | 4 + .../index.html" | 4 + .../index.html" | 4 + .../index.html" | 4 + 2023/10/07/git/index.html | 4 + .../index.html" | 4 + .../index.html" | 4 + 2023/10/19/open-source-9.19-10.19/index.html | 6 +- 2023/10/27/driver_develop/index.html | 4 + 2023/11/18/compilation_principle/index.html | 4 + 2023/11/26/cryptography/index.html | 4 + 2023/11/26/database/index.html | 4 + 2024/01/04/arch/index.html | 4 + about/index.html | 4 + archives/2022/10/index.html | 4 + archives/2022/11/index.html | 4 + archives/2022/12/index.html | 4 + archives/2022/index.html | 4 + archives/2023/01/index.html | 4 + archives/2023/02/index.html | 14 +- archives/2023/03/index.html | 14 +- archives/2023/06/index.html | 4 + archives/2023/08/index.html | 4 + archives/2023/09/index.html | 4 + archives/2023/10/index.html | 4 + archives/2023/11/index.html | 4 + archives/2023/index.html | 4 + archives/2023/page/2/index.html | 9 +- archives/2023/page/3/index.html | 14 +- archives/2023/page/4/index.html | 9 +- archives/2023/page/5/index.html | 4 + archives/2024/01/index.html | 4 + archives/2024/index.html | 4 + archives/index.html | 4 + archives/page/2/index.html | 4 + archives/page/3/index.html | 14 +- archives/page/4/index.html | 14 +- archives/page/5/index.html | 4 + index.html | 11 + search.xml | 15553 ++++++++-------- tag/index.html | 156 + tags/Java/index.html | 4 + tags/books/index.html | 4 + tags/intern/index.html | 18 + tags/labs/index.html | 4 + tags/mylife/index.html | 4 + "tags/os\347\253\236\350\265\233/index.html" | 18 + 81 files changed, 8358 insertions(+), 7844 deletions(-) create mode 100644 tag/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 8cc7fea3..3e4a1a46 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" @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + 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 88dcf320..3613b182 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" @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + 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 f53cb51b..5edeca29 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" @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + 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 612ca55d..6ca2ebc5 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" @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/2022/12/21/JavaWeb/index.html b/2022/12/21/JavaWeb/index.html index 2c02366e..ce913949 100644 --- a/2022/12/21/JavaWeb/index.html +++ b/2022/12/21/JavaWeb/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/2023/01/10/xv6$chap1/index.html b/2023/01/10/xv6$chap1/index.html index f19aad20..ba4d8b1a 100644 --- a/2023/01/10/xv6$chap1/index.html +++ b/2023/01/10/xv6$chap1/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -548,10 +552,10 @@

- + - + diff --git a/2023/01/10/xv6$chap2/index.html b/2023/01/10/xv6$chap2/index.html index 5bbbd488..a010ba16 100644 --- a/2023/01/10/xv6$chap2/index.html +++ b/2023/01/10/xv6$chap2/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -546,10 +550,10 @@

- + - + diff --git a/2023/01/10/xv6$chap3/index.html b/2023/01/10/xv6$chap3/index.html index 5c63f840..010d46b7 100644 --- a/2023/01/10/xv6$chap3/index.html +++ b/2023/01/10/xv6$chap3/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -621,10 +625,10 @@
- + - + diff --git a/2023/01/10/xv6$chap4/index.html b/2023/01/10/xv6$chap4/index.html index 915cff3b..26db42a4 100644 --- a/2023/01/10/xv6$chap4/index.html +++ b/2023/01/10/xv6$chap4/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -674,10 +678,10 @@
- + - + diff --git a/2023/01/10/xv6$chap5/index.html b/2023/01/10/xv6$chap5/index.html index 9f4c8af6..7cf9b8bc 100644 --- a/2023/01/10/xv6$chap5/index.html +++ b/2023/01/10/xv6$chap5/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -412,10 +416,10 @@

代码
- + - +
diff --git a/2023/01/10/xv6$chap6/index.html b/2023/01/10/xv6$chap6/index.html index ce26cfa5..57d8a885 100644 --- a/2023/01/10/xv6$chap6/index.html +++ b/2023/01/10/xv6$chap6/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -521,10 +525,10 @@

- + - + diff --git a/2023/01/10/xv6$chap7/index.html b/2023/01/10/xv6$chap7/index.html index 1a3290de..145c30f9 100644 --- a/2023/01/10/xv6$chap7/index.html +++ b/2023/01/10/xv6$chap7/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -508,7 +512,7 @@

- + diff --git a/2023/01/10/xv6$chap8/index.html b/2023/01/10/xv6$chap8/index.html index ebc82c8a..b368248d 100644 --- a/2023/01/10/xv6$chap8/index.html +++ b/2023/01/10/xv6$chap8/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/2023/01/10/xv6$chap9/index.html b/2023/01/10/xv6$chap9/index.html index 72254203..14d2984c 100644 --- a/2023/01/10/xv6$chap9/index.html +++ b/2023/01/10/xv6$chap9/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/2023/01/10/xv6/index.html b/2023/01/10/xv6/index.html index 37d3ab53..63b1efaf 100644 --- a/2023/01/10/xv6/index.html +++ b/2023/01/10/xv6/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/2023/02/25/cs144$else/index.html b/2023/02/25/cs144$else/index.html index 6cc0c1d6..648622b9 100644 --- a/2023/02/25/cs144$else/index.html +++ b/2023/02/25/cs144$else/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -347,10 +351,10 @@

其他
- + - +
diff --git a/2023/02/25/cs144$lab0/index.html b/2023/02/25/cs144$lab0/index.html index de2bef1c..821c45d1 100644 --- a/2023/02/25/cs144$lab0/index.html +++ b/2023/02/25/cs144$lab0/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -289,10 +293,10 @@

- + - + diff --git a/2023/02/25/cs144$lab1/index.html b/2023/02/25/cs144$lab1/index.html index e75b3002..ad12ad29 100644 --- a/2023/02/25/cs144$lab1/index.html +++ b/2023/02/25/cs144$lab1/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -379,10 +383,10 @@

- + - + diff --git a/2023/02/25/cs144$lab2/index.html b/2023/02/25/cs144$lab2/index.html index 1663fb0d..0b4f3b04 100644 --- a/2023/02/25/cs144$lab2/index.html +++ b/2023/02/25/cs144$lab2/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -372,10 +376,10 @@

- + - + diff --git a/2023/02/25/cs144$lab3/index.html b/2023/02/25/cs144$lab3/index.html index 55401bde..f965a5df 100644 --- a/2023/02/25/cs144$lab3/index.html +++ b/2023/02/25/cs144$lab3/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -330,10 +334,10 @@

- + - + diff --git a/2023/02/25/cs144$lab4/index.html b/2023/02/25/cs144$lab4/index.html index 1a632bc0..856fde36 100644 --- a/2023/02/25/cs144$lab4/index.html +++ b/2023/02/25/cs144$lab4/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -413,10 +417,10 @@

- + - + diff --git a/2023/02/25/cs144$lab5/index.html b/2023/02/25/cs144$lab5/index.html index 0ab8bb12..8aee0ef1 100644 --- a/2023/02/25/cs144$lab5/index.html +++ b/2023/02/25/cs144$lab5/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -367,10 +371,10 @@

- + - + diff --git a/2023/02/25/cs144$lab6/index.html b/2023/02/25/cs144$lab6/index.html index 07debf8f..53347fcc 100644 --- a/2023/02/25/cs144$lab6/index.html +++ b/2023/02/25/cs144$lab6/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -244,10 +248,10 @@

- + - + diff --git a/2023/02/25/cs144/index.html b/2023/02/25/cs144/index.html index 457b5936..6c39ba70 100644 --- a/2023/02/25/cs144/index.html +++ b/2023/02/25/cs144/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -265,10 +269,10 @@

- + - + 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 f5096db9..a28c956f 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" @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -199,10 +203,10 @@

状态机

- + - +
diff --git a/2023/03/13/cmu15445$lab0/index.html b/2023/03/13/cmu15445$lab0/index.html index 1faa7171..72c5c6b0 100644 --- a/2023/03/13/cmu15445$lab0/index.html +++ b/2023/03/13/cmu15445$lab0/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/2023/03/13/cmu15445$lab1/index.html b/2023/03/13/cmu15445$lab1/index.html index 8b9fa28d..bcdf1cf5 100644 --- a/2023/03/13/cmu15445$lab1/index.html +++ b/2023/03/13/cmu15445$lab1/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -462,10 +466,10 @@
- + - + diff --git a/2023/03/13/cmu15445$lab2/index.html b/2023/03/13/cmu15445$lab2/index.html index b489fff3..1d295752 100644 --- a/2023/03/13/cmu15445$lab2/index.html +++ b/2023/03/13/cmu15445$lab2/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -412,7 +416,7 @@

- + diff --git a/2023/03/13/cmu15445/index.html b/2023/03/13/cmu15445/index.html index 5277024f..b9b092c4 100644 --- a/2023/03/13/cmu15445/index.html +++ b/2023/03/13/cmu15445/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -276,10 +280,10 @@

Proje
- + - +
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 79218f1c..4b593fa1 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" @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/2023/06/21/comporgan/index.html b/2023/06/21/comporgan/index.html index 0b4a44c7..562fa67a 100644 --- a/2023/06/21/comporgan/index.html +++ b/2023/06/21/comporgan/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + 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 fcf19dcf..869d2b6a 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" @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/2023/08/12/kernel_compile/index.html b/2023/08/12/kernel_compile/index.html index da898fdc..63018e25 100644 --- a/2023/08/12/kernel_compile/index.html +++ b/2023/08/12/kernel_compile/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/2023/08/27/2023-os-comp/index.html b/2023/08/27/2023-os-comp/index.html index 39f3262c..3456dd38 100644 --- a/2023/08/27/2023-os-comp/index.html +++ b/2023/08/27/2023-os-comp/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -279,6 +283,10 @@

+ + # os竞赛 + +
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 f30bef34..c22ea24c 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" @@ -76,6 +76,8 @@ About + Tags +
@@ -97,6 +99,8 @@ About + Tags + 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 8d2619da..c29321eb 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" @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + 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 c0dead92..ecf1bfa0 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" @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + 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 bb287be5..9769e41e 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" @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/2023/10/07/git/index.html b/2023/10/07/git/index.html index 9a322129..455313c9 100644 --- a/2023/10/07/git/index.html +++ b/2023/10/07/git/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + 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 432d47f0..f54bb55b 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" @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + 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 d2d20408..02430214 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" @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + 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 acfef87a..903a22c1 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 @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -268,7 +272,7 @@

- # mylife + # intern diff --git a/2023/10/27/driver_develop/index.html b/2023/10/27/driver_develop/index.html index 7ba165df..0743fc57 100644 --- a/2023/10/27/driver_develop/index.html +++ b/2023/10/27/driver_develop/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/2023/11/18/compilation_principle/index.html b/2023/11/18/compilation_principle/index.html index 2d60dc5f..980d9777 100644 --- a/2023/11/18/compilation_principle/index.html +++ b/2023/11/18/compilation_principle/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/2023/11/26/cryptography/index.html b/2023/11/26/cryptography/index.html index 45810362..afe2a5d5 100644 --- a/2023/11/26/cryptography/index.html +++ b/2023/11/26/cryptography/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/2023/11/26/database/index.html b/2023/11/26/database/index.html index 0e94d495..3ab80bca 100644 --- a/2023/11/26/database/index.html +++ b/2023/11/26/database/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/2024/01/04/arch/index.html b/2024/01/04/arch/index.html index f18309c1..7a1f992c 100644 --- a/2024/01/04/arch/index.html +++ b/2024/01/04/arch/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/about/index.html b/about/index.html index 09fc9979..b8c09f5c 100644 --- a/about/index.html +++ b/about/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/archives/2022/10/index.html b/archives/2022/10/index.html index ad4f46f5..f8900e73 100644 --- a/archives/2022/10/index.html +++ b/archives/2022/10/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/archives/2022/11/index.html b/archives/2022/11/index.html index ad21f012..83fe26a9 100644 --- a/archives/2022/11/index.html +++ b/archives/2022/11/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/archives/2022/12/index.html b/archives/2022/12/index.html index 34c8b29b..efbc93b3 100644 --- a/archives/2022/12/index.html +++ b/archives/2022/12/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/archives/2022/index.html b/archives/2022/index.html index 03072272..adcd28f1 100644 --- a/archives/2022/index.html +++ b/archives/2022/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/archives/2023/01/index.html b/archives/2023/01/index.html index 5402a432..3e3f5d2a 100644 --- a/archives/2023/01/index.html +++ b/archives/2023/01/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/archives/2023/02/index.html b/archives/2023/02/index.html index a4aa61c5..a41f4ac1 100644 --- a/archives/2023/02/index.html +++ b/archives/2023/02/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -204,12 +208,9 @@

2023

+ -
- cs144 - 二月 25, 2023 -
@@ -218,9 +219,12 @@

2023

- +
+ cs144 + 二月 25, 2023 +
diff --git a/archives/2023/03/index.html b/archives/2023/03/index.html index 06fad730..25dee248 100644 --- a/archives/2023/03/index.html +++ b/archives/2023/03/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -149,12 +153,9 @@

2023

+ -
- CMU15445 - 三月 13, 2023 -
@@ -163,9 +164,12 @@

2023

- +
+ CMU15445 + 三月 13, 2023 +
diff --git a/archives/2023/06/index.html b/archives/2023/06/index.html index 79a719de..90c5e9c6 100644 --- a/archives/2023/06/index.html +++ b/archives/2023/06/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/archives/2023/08/index.html b/archives/2023/08/index.html index 807b01eb..acff2f2b 100644 --- a/archives/2023/08/index.html +++ b/archives/2023/08/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/archives/2023/09/index.html b/archives/2023/09/index.html index 9851b4e8..31410d6e 100644 --- a/archives/2023/09/index.html +++ b/archives/2023/09/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/archives/2023/10/index.html b/archives/2023/10/index.html index 1ca9622a..e07c24d5 100644 --- a/archives/2023/10/index.html +++ b/archives/2023/10/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/archives/2023/11/index.html b/archives/2023/11/index.html index a0fa76dd..06aba224 100644 --- a/archives/2023/11/index.html +++ b/archives/2023/11/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/archives/2023/index.html b/archives/2023/index.html index 5661bad2..f8ab7458 100644 --- a/archives/2023/index.html +++ b/archives/2023/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/archives/2023/page/2/index.html b/archives/2023/page/2/index.html index cf39f3fb..bccc009a 100644 --- a/archives/2023/page/2/index.html +++ b/archives/2023/page/2/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -238,12 +242,9 @@

2023

+ -
- CMU15445 - 三月 13, 2023 -
diff --git a/archives/2023/page/3/index.html b/archives/2023/page/3/index.html index 7db6d2e3..8ba628b7 100644 --- a/archives/2023/page/3/index.html +++ b/archives/2023/page/3/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -127,9 +131,12 @@

2023

- +
+ CMU15445 + 三月 13, 2023 +
@@ -226,12 +233,9 @@

2023

+ -
- cs144 - 二月 25, 2023 -
diff --git a/archives/2023/page/4/index.html b/archives/2023/page/4/index.html index 483cd64c..3ef83cfd 100644 --- a/archives/2023/page/4/index.html +++ b/archives/2023/page/4/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -127,9 +131,12 @@

2023

- +
+ cs144 + 二月 25, 2023 +
diff --git a/archives/2023/page/5/index.html b/archives/2023/page/5/index.html index 2ca29a07..37ea05a0 100644 --- a/archives/2023/page/5/index.html +++ b/archives/2023/page/5/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/archives/2024/01/index.html b/archives/2024/01/index.html index 8fe059b7..69fdc6bf 100644 --- a/archives/2024/01/index.html +++ b/archives/2024/01/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/archives/2024/index.html b/archives/2024/index.html index 83ccebf6..ac37986c 100644 --- a/archives/2024/index.html +++ b/archives/2024/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/archives/index.html b/archives/index.html index b3aced21..1cb450b1 100644 --- a/archives/index.html +++ b/archives/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/archives/page/2/index.html b/archives/page/2/index.html index 7b929932..1936ea76 100644 --- a/archives/page/2/index.html +++ b/archives/page/2/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/archives/page/3/index.html b/archives/page/3/index.html index 82531f02..ba8e4464 100644 --- a/archives/page/3/index.html +++ b/archives/page/3/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -127,12 +131,9 @@

2023

+ -
- CMU15445 - 三月 13, 2023 -
@@ -141,9 +142,12 @@

2023

- +
+ CMU15445 + 三月 13, 2023 +
diff --git a/archives/page/4/index.html b/archives/page/4/index.html index 5a9b50fb..762bb9db 100644 --- a/archives/page/4/index.html +++ b/archives/page/4/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -127,12 +131,9 @@

2023

+ -
- cs144 - 二月 25, 2023 -
@@ -141,9 +142,12 @@

2023

- +
+ cs144 + 二月 25, 2023 +
diff --git a/archives/page/5/index.html b/archives/page/5/index.html index 5dcb5f51..22f4bd45 100644 --- a/archives/page/5/index.html +++ b/archives/page/5/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/index.html b/index.html index 9d6df95e..5b5de822 100644 --- a/index.html +++ b/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -135,6 +139,13 @@ + + + + + + + diff --git a/search.xml b/search.xml index 2c92cef6..6db01ac6 100644 --- a/search.xml +++ b/search.xml @@ -68,2096 +68,974 @@

学到了什么

感觉在这里也懒得说太多了,直接去看项目仓库吧()

]]> + + os竞赛 + - JavaWeb - /2022/12/21/JavaWeb/ - 第一部分 Java基础

JUnit单元测试

JUnit是白盒测试。

-

简要使用步骤

定义测试类

包含各种测试用例。

-

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

-

定义测试方法

测试方法可以独立运行。

-

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

-

给方法加@Test标签

加入JUnit依赖包

具体细节

断言

Assert.assertEquals(3,result);
- -

@Before @After

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

-

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

-

反射

反射是框架设计的灵魂。

-

Java对象创建的三个阶段

image-20221205194807395

-

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

-

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

-

获取Class对象

有三种方式。

-

Class.forName(“类的全名”)

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

-

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

-

类型.class

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

-

对象.getClass()

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

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

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

/*输出
class Student
class Student
class Student
true true*/
- -

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

-

使用Class对象

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

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

private Student() {

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

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

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

字段

获取字段

常用方法:

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

public java.lang.String Student.name

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

构造方法

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

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

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

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

方法

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

使用反射的案例

image-20221205205122805

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

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

注解

image-20221205211932790

-

image-20221205212028989

-

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

-

生成doc文档

javadoc XXX.java
- -

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

-

JDK预定义注解

image-20221205212443918

-

自定义注解

注解类的本质

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

本质上

-
public @interface Override{}
- -

等价于

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

注解的属性

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

-

image-20221205213135522

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

元注解

描述注解的注解

-

image-20221205213324052

-

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

-

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

-

CLASS:保留到字节码文件

-

RUNTIME:被读到

-

自定义的注解一般都取RUNTIME

-

在程序中获取注解属性

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

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

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

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

-

使用案例:测试框架

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

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

-

image-20221205215017285

-

然后编写test方法:

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

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

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

image-20221205220053258

-

第二部分 数据库

Mysql

登录方式

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

文件结构

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

-

image-20221205221041508

-

image-20221205221055114

-

SQL语法

image-20221205221216882

-

SQL有四种语句类型

-

image-20221205221246672

-

DDL 操作数据库、表

操纵数据库

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

操纵表

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

*注:

-
    -
  1. mysql的数据类型表

    -

    image-20221205222127740

    -

    其中:

    -

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

    -

    ② 关于三个时间类型

    -

    image-20221205222307284

    -

    所以timestamp常用作插入时间。

    -

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

    -

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

    -

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

    + 计算机体系结构 + /2024/01/04/arch/ + 01 基本知识
      +
    1. SISD、SIMD、MIMD、向量处理器的基本概念

      +

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

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

    DML 增删改表中数据

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

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

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

    image-20221205224600869

    -

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

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

    DQL 查询表中记录

    语法

    image-20221205224802814

    -

    基础查询

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

    select distinct # 去重
    address
    from
    students;

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

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

    条件查询

    运算符
      -
    1. 基本运算符

      -

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

      +
    2. 发射与流出

      +

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

      +
        +
      1. 发射(Issue):
          +
        • “发射”指的是将指令从指令流中发送到处理器的执行部件或执行单元,以进行实际的执行。
        • +
        • 发射阶段通常是在取指令和解码指令之后,将指令发送到执行单元的过程。
        • +
        • 多发射处理器意味着多条指令可以同时进入执行阶段,通过并行执行提高处理器的性能
        • +
      2. -
      3. 逻辑运算符

        -

        AND、OR

        +
      4. 流出(Out-of-Order Execution):
          +
        • “流出”是指处理器在执行过程中允许指令乱序执行,即不按照它们在程序中的原始顺序执行。在乱序执行的情况下,处理器会通过重新排序指令来填充执行单元的空闲周期,以提高整体性能。
        • +
        • 多流出处理器采用乱序执行的方式,允许在执行单元空闲时执行无关的指令,以最大程度地利用执行单元的并行性。
        • +
      5. -
      6. BETWEEN AND

        +
      +

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

    3. -
    4. IN后跟集合

      -

      image-20221205230824086

      +
    5. tensor 张量

      +

      sparse tensor 稀疏张量

    6. -
    7. IS、IS NOT

      -

      image-20221205230902110

      +
    8. 异构计算

      +

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

      +

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

    9. -
    10. LIKE 模糊查询

      -

      类似正则使用占位符匹配

      -

      image-20221205231037021

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

    各种函数一样的东西

    排序函数
    order by 排序字段1 排序方式1,排序字段2 排序方式2;
    - +

    02 现代处理器体系结构

    img

    +

    img

    +

    例题

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

    1

    img

    +

    注意几点:

      -
    1. 默认升序。

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

    2

    img

    +

    img

    +

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

    +

    img

    +

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

    +

    题型2 换名/消除WAR WAW

    1

    img

    +

    2

    img

    +

    题型3 记分牌

    img

    +

    这里的结构相关值得注意

    +

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

    +
      +
    1. 指令状态表

      +
        +
      1. 流出

        +

        无结构冲突、无WAW冲突

        +

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

        +

        ② 由①可知全部流出

      2. -
      3. ASC、DESC

        +
      4. 读操作数

        +

        操作数可用时完成该阶段

        +

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

        +

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

      5. -
      6. 多关键字排序

        -

        image-20221205231606255

        -

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

        +
      7. 执行

        +

        纯纯的算术

        +

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

        +

        ② 全部都结了

      8. -
      -
      聚合函数

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

      -

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

      -
      select count(ifnull(math,0)) from students;
      - -
        -
      1. count 计算个数

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

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

        +
      2. 写结果

        +

        不存在WAR则写入

        +

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

        +

        ② 除了DIV全部结了

      3. -
      4. max、min

        +
    2. -
    3. sum 求和

      +
    4. 功能部件状态表

      +

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

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

      +
    7. 寄存器状态表

      +

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

    -
    分组查询

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

      img

      +

      img

      +

      题型4 Tomasulo算法

      3段流水

      +
        +
      1. 流出

        +
          +
        1. 没有结构冲突就流出,填进保留站

          +

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

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

          +
        -
        分页查询

        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

          +

          保留站有以下字段:

          +
            +
          • Op:操作

          • -
          • 注册驱动

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

          • -
          • 获取数据库连接对象 Connection

            +
          • Vj,Vk:源操作数值

            +

            load的Vk保存偏移量

          • -
          • 定义sql语句

            +
          • Busy

          • -
          • 获取执行sql语句的对象 Statement

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

          • -
          • 执行sql,接收返回的结果

            +
          • Qi:寄存器状态表

            +

            存放要写入它的保留站ID

          • -
          • 处理结果

            +
        2. -
        3. 释放资源

          +
        4. 执行

          +

          两个操作数就绪后就执行

          +
        5. +
        6. 写结果

          +

          计算完毕后由CDB传送

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

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

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

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

        详解各个类

        DriverManager

        注册驱动

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

        -

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

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

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

        -

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

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

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

        -
        -

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

        -

        image-20221220151045275

        -

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

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

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

        -

        image-20221220151425953

        -

        Connection

        数据库连接对象。

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

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

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

        Statement

        -

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

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

        ResultSet

        封装查询结果集。

        -

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

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

        使用实例:

        -

        image-20221220164414363

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

        注意:

        +

        例题

        img

        +

        img

        +

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

        +

        img

        +

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

        +

        img

        +

        img

        +

        img

        +

        img

        +

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

        +

        img

        +

        题型5 Tomasulo+前瞻执行

        4段流水

          -
        1. 这东西也得Close

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

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

          而应该这样:

          -
          return resultSet.next();
        4. -
        -

        PreparedStatement

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

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

        -

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

        -
        使用步骤

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

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

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

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

        它更安全且效率更高。

        -

        JDBC工具类

        书写

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

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

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

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

        使用

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

        JDBC控制事务

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

        -

        image-20221220214249168

        -

        image-20221220224401302

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

        数据库连接池

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

        -

        image-20221222212904151

        -

        C3P0

        基本使用

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

        -
        设置配置文件

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

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

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

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

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

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

        -

        ComboPooledDataSource有一个含参构造器:

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

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

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

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

        Druid

        基本使用

        设置配置文件

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

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

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

        定义工具类

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

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

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

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

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

        public static DataSource getDataSource(){
        return ds;
        }

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

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

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

        使用同上的JDBCUtils

        -

        Spring JDBC

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

        -

        使用方法

        带参(PreparedStatement)

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

        DML

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

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

        DQL

        提供了三种方法。

        -
        queryForMap

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

        -

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

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

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

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

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

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

        -
        原始一点的

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

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

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

        使用包装好的BeanPropertyRowMapper类。

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

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

        注意:

        +
      2. 流出

          -
        1. 要求包装的class,比如说Client,必须要有public的无参构造器
        2. -
        3. Java的那个被包装类的字段最好使用基本数据类型,而使用引用类型,如Integer,Double等等等。因为如果使用基本数据类型,当表中数据为null时会报错。
        4. -
        5. 要求被包装的class的字段名称一定要与数据库的一模一样,大小写可以不一样。
        6. -
        7. 要求被包装的class的字段一定要是可以修改的。也就是说,要么public,要么提供set方法。
        8. +
        9. 保留站&ROB都有空闲才流出

          +

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

          +
        10. +
        11. 具体填什么看操作数有没有就绪

          +
        -
        queryForObject

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

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

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

        Web概述

          -
        • JavaWeb:

          +

          保留站有以下字段:

            -
          • 使用Java语言开发基于互联网的项目
          • -
          +
        • Op:操作

        • -
        • 软件架构:

          -
            -
          1. C/S: Client/Server 客户端/服务器端
              -
            • 在用户本地有一个客户端程序,在远程有一个服务器端程序
            • -
            • 如:QQ,迅雷…
            • -
            • 优点:
                -
              1. 用户体验好
              2. -
              +
            • Qj,Qk:操作数保留站号

            • -
            • 缺点:
                -
              1. 开发、安装,部署,维护 麻烦
              2. -
              +
            • Vj,Vk:源操作数值

              +

              load的Vk保存偏移量

            • -
            +
          2. Busy

          3. -
          4. B/S: Browser/Server 浏览器/服务器端
              -
            • 只需要一个浏览器,用户通过不同的网址(URL),客户访问不同的服务器端程序
            • -
            • 优点:
                -
              1. 开发、安装,部署,维护 简单
              2. -
              +
            • A:存放立即数字段 or 有效地址,仅用于load和store缓冲器

            • -
            • 缺点:
                -
              1. 如果应用过大,用户的体验可能会受到影响
              2. -
              3. 对硬件要求过高
              4. -
              +
            • Qi:寄存器状态表

              +

              存放要写入它的保留站ID

          5. -
          +
        • 执行

          +

          两个操作数就绪后就执行

        • -
        • B/S架构详解

          -
            -
          • 资源分类:

            +
          • 写结果

              -
            1. 静态资源:
                -
              • 使用静态网页开发技术发布的资源。
              • -
              • 特点:
                  -
                • 所有用户访问,得到的结果是一样的。
                • -
                • 如:文本,图片,音频、视频, HTML,CSS,JavaScript
                • -
                • 如果用户请求的是静态资源,那么服务器会直接将静态资源发送给浏览器。浏览器中内置了静态资源的解析引擎,可以展示静态资源
                • -
                +
              • 写入ROB,CDB传送ROB编号到保留站
              • +
              • 释放产生该结果的保留站
              • +
            +

            ROB字段:

            +
              +
            • 指令类型

            • -
            +
          • 目标地址

            +

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

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

              +

              前瞻结果

              +
            • +
            • 就绪字段

              +

              结果是否就绪

          • +
          • 指令确认

            +

            分支结果出来后确认

            +
              +
            1. 猜测对 写入寄存器/存储器,释放ROB
            2. +
            3. 猜测错 从另一条路径开始重新执行,清空ROB
          • -
          • 我们要学习动态资源,必须先学习静态资源!

            -
          • -
          • 静态资源:

            -
              -
            • HTML:用于搭建基础网页,展示页面的内容
            • -
            • CSS:用于美化页面,布局页面
            • -
            • JavaScript:控制页面的元素,让页面有一些动态的效果
            • -
            -
          • -
          +
      +

      例题

      img

      +

      img

      +

      img

      +

      img

      +

      img

      +

      img

      +

      题型6 超标量实现

      例题

      img

      +

      img

      +

      img

      +

      img

      +

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

      +

      img

      +

      img

      +

      img

      +

      img

      +

      img

      +

      题型7 循环展开

      具体步骤:

      +
        +
      1. 依题意展开
      2. +
      3. 去除多余的BNE、合并所有DADDUI
      4. +
      5. 寄存器换名消除名相关
      6. +
      7. 重排序消除数据相关
      8. +
      +

      1

      img

      +

      img

      +

      img

      +

      img

      +

      img

      +

      img

      +

      img

      +

      img

      +

      img

      +

      2

      img

      +

      img

      +

      img

      +

      题型8 VIEW技术

      例题

      img

      +

      img

      +

      img

      +

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

      +

      img

      +

      题型9 软流水

      例题

      img

      +

      img

      +

      img

      +

      img

      +

      指令级并行

      概念

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

        +
          +
        1. 基于硬件的动态开发
        2. +
        3. 基于软件的静态开发
        4. +
      2. - -

        静态网页概述

        练习:用纯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. 流水线CPI

          +

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

        3. -
        4. 图片适应屏幕宽度

          -

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

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

        表单

        注意

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

          -

          image-20221223161847622

          -

          也即一定要有属性name。

          +
        2. 理想CPI是衡量流水线最高性能

        3. -
        4. 关于from的属性

          -

          image-20221223162035569

          +
        5. IPC:每个时钟周期完成的指令数

          +

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

        6. -
        7. 一般都这么写

          -

          image-20221223165046277

          +
        8. 基本程序块:一串没有分支和跳转转入点的指令块

        -

        练习

        image-20221223171603366

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

        </body>
        </html>
        - -

        CSS

        盒子模型

        image-20221223204733649

        -

        练习:注册页面

        image-20221223223128861

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

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

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

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

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

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

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

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

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


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

        }

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

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

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

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

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

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

        .choice{
        font-size: 15px;
        }

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

        #img_check{
        vertical-align:middle;
        }

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

        JavaScript

        对象

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

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

          +

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

          +

          相关与并行

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

          +

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

          +

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

          +

          约定先执行i再执行j

          +
            +
          1. 数据相关

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

            2. -
            3. 方法:
              encodeURI():url编码
              decodeURI():url解码

              -

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

              -

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

              -
                -
              • 逐一判断每一个字符是否是数字,直到不是数字为止,将前边数字部分转为number
                isNaN():判断一个值是否是NaN
                  -
                • NaN六亲不认,连自己都不认。NaN参与的==比较全部问false
                • -
                +
              • 具有传递性

              • -
              -

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

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


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

              var jscode = "alert(123)";
              eval(jscode);
            4. -
            -

            DOM

            image-20221224162341993

            -

            image-20221224162540306

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

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

            -

            image-20221224164239507

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

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

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

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

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

            table.appendChild(row);
            }

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

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

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

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

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

            image-20221225192124809

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

            </script>
            </body>
            </html>
            - -

            BOM

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

            -

            image-20221224152804747

            -

            Window对象包含DOM对象。

            -

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

            -
            Window

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

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

              -

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

              +
            2. 反映数据流动关系,即如何从生产者流动到消费者

            3. -
            4. 与开关有关的方法

              -

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

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

            6. -
            7. 定时器

              -
              //只执行一次
              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);
            8. -
            -
            属性
              -
            1. 获取其他BOM对象

              -

              history、location、navigator、screen

              +
            2. 解决方法

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

                +

                调度

              2. -
              3. 获取DOM对象

                -

                document

                +
              4. 变换代码消除相关关系

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

                -

                location.reload方法

              2. -
              3. 设置或返回完整的url

                -

                location.href属性

                +
              4. 检测方法

                +

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

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

              Bootstrap

              web前端框架

              -

              image-20221225160246028

              -

              快速入门

              image-20221225161243229

              -

              基本模板:

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

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

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

              响应式布局

              实现依赖于栅格系统。

              -

              栅格系统

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

              -

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

              -
              基本原理
                -
              1. 定义容器。相当于之前的table
              2. + +
              3. 名相关

                +

                img

                +
                  +
                1. 分类

                  +
                    +
                  1. 反相关:先读后写,也即i读的名与j写的名相同
                  2. +
                  3. 输出相关:i和j写的名相同
                  -
                    -
                  • 容器分类

                    +
                  • +
                  • 解决方法:换名技术,可以编译器静态实现 or 硬件动态实现

                    +

                    img

                    +
                  • +
                  • 相关问题

                    +

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

                      -
                    1. container:两边留白
                    2. -
                    3. container-fluid:每一种设备都是100%宽度
                    4. +
                    5. WAR(反相关)
                    6. +
                    7. WAW(输出相关)
                  • -
                  -
                    -
                  1. 定义行。相当于之前的tr 样式:row
                  2. -
                  3. 定义元素。指定该元素在不同的设备上,所占的格子数目。样式:col-设备代号-格子数目
                  -
                    -
                  • 设备代号:

                    +
                  • +
                  • 数据冲突

                    +

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

                      -
                    1. xs:超小屏幕 手机 (<768px):col-xs-12
                    2. -
                    3. sm:小屏幕 平板 (≥768px)
                    4. -
                    5. md:中等屏幕 桌面显示器 (≥992px)
                    6. -
                    7. lg:大屏幕 大桌面显示器 (≥1200px)
                    8. +
                    9. RAW(数据相关)

                      +

                      也即i写j读

                      +
                    10. +
                    11. WAW(输出相关)

                      +

                      也即i写j写

                      +

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

                      +

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

                      +
                    12. +
                    13. WAR(反相关)

                      +

                      也即i读j写

                      +

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

                      +
                    14. +
                    +
                  • +
                  • 控制相关

                    +

                    由分支指令引起

                    +
                +

                调度

                img

                +

                img

                +

                动态调度

                基本思想

                img

                +

                img

                +

                img

                +

                img

                +

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

                +

                img

                +

                记分牌动态调度算法

                  +
                1. 基本思想

                  +

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

                  +
                2. +
                3. 基本结构

                  +

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

                  +
                    +
                  1. 指令状态表

                    +

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

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

                    +

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

                    +
                      +
                    • Busy:yes/no
                    • +
                    • Op:操作编码
                    • +
                    • Fi:目的寄存器编号
                    • +
                    • Fj,Fk:源寄存器编号
                    • +
                    • Qj,Qk:Fj和Fk的功能部件
                    • +
                    • Rj,Rk:Fj和Fk是否就绪且还没被取走
                    -
                    使用方法
                    <!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;
                    }
                    - -

                    注意点

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

                    +
                  4. +
                  5. 结果寄存器状态表

                    +

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

                    +

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

                    +
                  6. +
                  +
                4. +
                5. 执行流程

                  +

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

                    -
                  1. text-align

                    -

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

                    +
                  2. 流出

                    +

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

                  3. -
                  4. 关于“容器”的理解

                    -

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

                    -

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

                    -

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

                    -

                    ① container中可以嵌套container-fluid。

                    -

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

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

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

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

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

                    -

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

                    -

                    image-20221225183910393

                    -

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

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

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

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

                    +
                  5. 读操作数

                    +

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

                  6. -
                  7. 关于hr标签

                    -

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

                    +
                  8. 写结果

                    +

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

                  -

                  XML

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

                  -

                  image-20221225220356932

                  -

                  快速入门

                    -
                  • 基本语法:
                      1. xml文档的后缀名 .xml
                    -  2. xml第一行必须定义为文档声明
                    -  3. xml文档中有且仅有一个根标签
                    -  4. 属性值必须使用引号(单双都可)引起来
                    -  5. 标签必须正确关闭
                    -  6. xml标签名称区分大小写
                    -
                  • -
                  -
                  <?xml version="1.0" encoding="utf-8" ?>

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

                  细说

                  语法

                  文档声明

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

                  -
                  属性

                  id属性值唯一

                  -
                  文本

                  image-20221225222029698

                  -

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

                  -

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

                  -
                  <code>

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

                  </code>
                  - -

                  约束

                  只能写约束文件内的标签

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

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

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

                  <!DOCTYPE students [

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


                  ]>
                  <students>

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

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

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

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

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

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

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

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

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


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

                  </students>
                  - -

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

                  -

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

                  -

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

                  -

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

                  -

                  解析

                  image-20221225234106078

                  -

                  解析方式有两种方法。

                  -
                  解析方法
                  DOM

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

                  -

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

                  -
                  SAX

                  逐行读取,基于事件驱动

                  -

                  不占内存;但只能读取

                  -
                  解析工具

                  image-20221225234649287

                  -

                  主要学习Jsoup。

                  -
                  快速入门

                  image-20221225234813135

                  -

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

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

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

                      +

                      img

                    • -
                    +
                  +

                  Tomasulo算法

                    +
                  1. 核心思想

                    +

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

                    +

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

                    +

                    img

                  2. -
                  3. Document:文档对象。代表内存中的dom树

                    +
                  4. 基本结构

                    +
                      +
                    1. 保留站

                      +

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

                      +

                      img

                      +

                      img

                      +

                      故而,需要有以下字段:

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

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

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

                        +

                        load的Vk保存偏移量

                        +
                      • +
                      • Busy

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

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

                        +

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

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

                      +
                    4. 公共数据总线CDB

                      +

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

                    5. -
                    6. Element:元素对象

                      +
                    7. load缓冲器和store缓冲器

                        -
                      1. 获取子元素对象
                      2. +
                      3. load缓冲器
                          +
                        1. 存放用于计算有效地址的分量
                        2. +
                        3. 记录正在进行的load访存
                        4. +
                        5. 保存buffer等待CDB传输
                        -

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

                        -
                          -
                        • getElementById(String id):根据id属性值获取唯一的element对象
                        • -
                        • getElementsByTag(String tagName):根据标签名称获取元素对象集合
                        • -
                        • getElementsByAttribute(String key):根据属性名称获取元素对象集合
                        • -
                        • getElementsByAttributeValue(String key, String value):根据对应的属性名和属性值获取元素对象集合
                        • -
                        -
                          -
                        1. 获取属性值
                        2. + +
                        3. store缓冲器
                            +
                          1. 存放用于计算有效地址的分量
                          2. +
                          3. 记录正在进行的store访存,如目标地址以及是否已有数据
                          4. +
                          5. 保存buffer等待CDB传输
                          -
                            -
                          • String attr(String key):根据属性名称获取属性值
                          • -
                          -
                            -
                          1. 获取文本内容
                          2. +
                          -
                            -
                          • String text():获取字标签的所有纯文本内容
                          • -
                          • String html():获取标签体的所有内容(包括字标签的字符串内容)
                          • -
                        4. -
                        5. Node:节点对象

                          -
                            -
                          • 是Document和Element的父类
                          • -
                          +
                        6. 浮点寄存器FP

                          +

                          img

                          +
                        7. +
                        8. 指令队列

                          +

                          FIFO

                          +
                        9. +
                        10. 运算部件

                          +

                          浮点加法器、浮点乘法器

                        -
                        快速查找
                          -
                        1. 使用选择器selector

                          -

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

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

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

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

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

                          }

                          }
                        2. -
                        3. 使用XPath

                          -

                          XPath:xml路径语言。

                          -

                          XPath API文档

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

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

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

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

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

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

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

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

                          }
                        4. -
                        -

                        第四部分 JavaWeb核心

                        Tomcat

                        概述

                        概述

                        image-20221226154531990

                        -

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

                        -

                        tomcat目录结构

                        image-20221226155107723

                        -

                        image-20221226155143920启动

                        -

                        启动时出现的问题

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

                        -

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

                        -

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

                        -

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

                        -

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

                        -

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

                        -

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

                        -

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

                        -

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

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

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

                        -

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

                        -

                        image-20221226170930763

                        -

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

                        -

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

                        -

                        image-20221226172653916

                        -

                        配置

                          -
                        • 部署项目的方式:

                          +
                        • +
                        • 寄存器换名实现

                          +

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

                          +
                        • +
                        • 特点

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

                            -
                              -
                            • war包会自动解压缩
                            • -
                            +
                          2. 冲突检测与指令执行是分布的

                            +

                            通过保留站和CDB实现

                            +

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

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

                            +
                          5. 消除了WAW和WAR

                          -
                          然后之后访问时输入`localhost/web/JavaWeb.html`即可
                          -
                          -* docBase:项目存放的路径
                          -* path:虚拟目录
                          -
                          -
                            -
                          1. 在conf\Catalina\localhost创建任意名称的xml文件。在文件中编写
                            <Context docBase="C:\aWorkSpace\Projects\Java\JavaWeb" />
                              -
                            • 虚拟目录:xml文件的名称
                            • -
                          2. -
                          -
                          注意,该方法是热部署的。也就是说,可以不关闭服务器的情况下,去增删xml文件,会马上变化,而不是像上面两种方式一样重启生效。
                          -
                          +
                        • 执行步骤

                          +

                          3段流水

                          +
                            +
                          1. 流出

                            +

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

                            +
                          2. +
                          3. 执行

                            +

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

                            +

                            img

                            +
                          4. +
                          5. 写结果

                            +

                            计算完毕后由CDB传送

                          6. -
                        -

                        动态项目目录结构

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

                        -

                        image-20221226221811747

                        -

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

                        -

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

                        -

                        image-20221226221933346

                        -

                        tomcat集成到IDEA

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

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

                        -
                        创造项目

                        image-20221226235520519

                        -

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

                        -

                        最后的目录结构:

                        -

                        image-20221226235607604

                        -

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

                        -
                        加入tomcat

                        1.

                        -

                        TOMCAT -> IDEA

                        -

                        2.

                        -

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

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

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

                        -

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

                        -

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

                        -

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

                        -

                        image-20221227003805955

                        -

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

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

                        run-edit configuration-tomcat

                        -
                        配置文件

                        image-20221230221339275

                        -

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

                        -

                        Servlet

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

                        -

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

                        -

                        image-20221227154053743

                        -

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

                        -

                        image-20221227154222612

                        -

                        快速入门

                        image-20221227154841102

                        -

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

                        -
                        -

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

                        -
                        -
                        -

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

                        -

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

                        -
                        -

                        原理

                        执行原理

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

                        生命周期

                        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!!!");
                        }
                        }

                        +
                      +

                      基于硬件的前瞻执行

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      多指令流出

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      超标量实现

                      img

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

                        +

                        整数型:load、store、分支

                        +

                        浮点型:可能各种运算吧

                        +
                      2. +
                      3. 假设所有浮点指令都是加法,执行时间3个时钟周期,且图中整数总在浮点前

                        +
                      4. +
                      +

                      img

                      +

                      img

                      +

                      img

                      +

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

                      +

                      img

                      +

                      基于静态调度

                      img

                      +

                      基于动态调度

                      img

                      +

                      img

                      +

                      img

                      +

                      VLIW技术

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      基本指令调度和循环展开

                      img

                      +

                      img

                      +

                      指令调度

                      img

                      +

                      img

                      +

                      img

                      +

                      循环展开

                      img

                      +

                      img

                      +

                      软流水

                      img

                      +

                      img

                      +

                      03 AI处理器

                      并行体系结构

                      分类

                      SISD

                      img

                      +

                      SIMD

                      img

                      +

                      img

                      +

                      MIMD

                      img

                      +

                      向量体系结构

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

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

                      +

                      GPU

                      概念

                      img

                      +

                      img

                      +

                      GPU体系结构

                      img

                      +

                      img

                      +

                      GPU计算

                      img

                      +

                      CUDA编程

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

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

                      +

                      img

                      +

                      img

                      +

                      下面两个标题反了额

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

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

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      例题

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      访存优化

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      真没懂。。。。

                      +

                      img

                      +

                      img

                      +

                      img

                      +

                      真没看懂

                      +

                      GPU控制流和指令优化

                      img

                      +

                      AI开发

                      DL框架

                      img

                      +

                      img

                      +

                      PyTorch

                      img

                      +

                      img

                      +

                      img

                      +

                      算子开发

                      img

                      +

                      img

                      +

                      TODO接下来有兴趣看吧

                      +

                      模型开发

                      img

                      +

                      04 自动驾驶体系结构

                      img

                      +

                      img

                      +

                      img

                      +

                      img

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

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

                      +

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

                      +

                      Trie

                      +

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

                      +

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

                      +

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

                      +

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

                      +

                      image-20230312150245669

                      +
                      +

                      心得

                      感想

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

                      +

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

                      +

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

                      +

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

                      +

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

                      +

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

                      +

                      错误集锦

                      sb错误

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

                      +

                      image-20230318154940622

                      +

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

                      +

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

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

                      这两个方法的区别就是,当使用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>
                      +

                      如上是spring2023的代码模板。

                      +

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

                      +

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

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

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

                      +

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

                      +

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

                      +
                      错误使用unique_ptr::get

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

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

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

                      +
                      共享unique_ptr

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

                      +

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

                      +
                      代码规范

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

                      +
                      $ make format
                      $ make check-lint
                      $ make check-clang-tidy-p0
                      + +
                      自测

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

                      +

                      image-20230318165521340

                      +
                      In file included from /autograder/bustub/src/common/bustub_instance.cpp:17:
                      /autograder/bustub/src/include/common/bustub_instance.h:30:10: fatal error: 'libfort/lib/fort.hpp' file not found
                      #include "libfort/lib/fort.hpp"
                      + +

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

                      +

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

                      +

                      image-20230318173941576

                      +

                      image-20230318174102171

                      +

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

                      +

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

                      +

                      COW-Trie

                      +

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

                      +
                      +

                      先放个通关截图~

                      +

                      image-20230322235159843

                      +

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

                      +

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

                      +

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

                      +

                      Task1 cow-trie

                      +

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

                      +

                      下面举例说明

                      +

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

                      +

                      image-20230323000513767

                      +

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

                      +

                      image-20230323000601882

                      +

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

                      +

                      image-20230323000658620

                      +

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

                      +

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

                      +
                      +

                      感想

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

                      +

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

                      +

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

                      +

                      错误集锦

                      const转移

                      trie.h中:

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

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

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

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

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

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

                      +

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

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

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

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

                      servlet代码同上。

                      -

                      最终在网页中点击提交

                      -

                      image-20221230172048250

                      -

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

                      -

                      为啥会这样呢?

                      -

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

                      +

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

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

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

                      +

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

                      -

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

                      -

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

                      -

                      image-20221230215927404

                      -

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

                      +

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

                      -
                      深层一些的问题
                      分成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的逻辑
                      }
                      }
                      +
                      make_shared

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

                      +
                      +

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

                      +
                      +
                      一个奇妙的报错

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

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

                      就类似于可以这么写:

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

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

                      +
                      std::make_shared<TrieNode>()
                      -

                      于是最后就融合入httpservlet了。

                      -

                      url-pattern配置

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

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

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

                      service参数

                      image-20230102005833327

                      -

                      http协议

                      概述

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

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

                        -
                      • -
                      • 特点:

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

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

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

                        历史版本:

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

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

                      -

                      数据格式:

                      -
                        -
                      1. 请求行
                        请求方式 请求url 请求协议/版本
                        GET /login.html HTTP/1.1

                        -
                          -
                        • 请求方式:
                            -
                          • HTTP协议有7中请求方式,常用的有2种
                              -
                            • GET:
                                -
                              1. 请求参数在请求行中【在url后】
                              2. -
                              3. 请求的url长度有限制的
                              4. -
                              5. 不太安全
                              6. -
                              -
                            • -
                            • POST:
                                -
                              1. 请求参数在请求体中
                              2. -
                              3. 请求的url长度没有限制的
                              4. -
                              5. 相对安全
                              6. -
                              -
                            • -
                            -
                          • -
                          -
                        • -
                        -
                      2. -
                      3. 请求头:客户端浏览器告诉服务器一些信息
                        请求头名称: 请求头值

                        -
                          -
                        • 常见的请求头:

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

                            -
                          2. -
                          3. Accept:可以支持的响应格式

                            -
                          4. -
                          5. Accept-language:可以支持的语言环境

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

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

                            -
                          10. -
                          -
                        • -
                        -
                      4. -
                      5. 请求空行
                        空行,就是用于分割POST请求的请求头,和请求体的。

                        -
                      6. -
                      7. 请求体(正文):

                        -
                          -
                        • 封装POST请求消息的请求参数的
                        • -
                        -
                      8. -
                      -

                      字符串格式:

                      -
                      //请求行
                      POST /login.html HTTP/1.1
                      //请求头
                      Host: localhost
                      User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0
                      Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
                      Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
                      Accept-Encoding: gzip, deflate
                      Referer: http://localhost/login.html
                      Connection: keep-alive
                      Upgrade-Insecure-Requests: 1
                      //请求空行

                      //请求体
                      username=zhangsan
                      +

                      漏了个std::呃呃。

                      +
                      显式保存is_value_node_

                      trie.cpp RemoveHelper()中:

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

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

                      否则会:

                      +

                      image-20230323114435061

                      +

                      查看trie_test.cpp的代码:

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

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

                      ASSERT_EQ(trie.Get<uint32_t>("te"), nullptr);
                      ASSERT_EQ(trie.Get<uint32_t>("tes"), nullptr);
                      ASSERT_EQ(trie.Get<uint32_t>("test"), nullptr);
                      }
                      +

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

                      +

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

                      +

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

                      +

                      image-20230323115902073

                      +

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

                      +

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

                      +

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

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

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

                      响应消息:服务器端发送给客户端的数据

                      -

                      数据格式:

                      -
                        -
                      1. 响应行

                        -
                          -
                        1. 组成:协议/版本 响应状态码 状态码描述 HTTP/1.1 200 OK

                          -
                        2. -
                        3. 响应状态码:服务器告诉客户端浏览器本次请求和响应的一个状态, 状态码都是3位数字. 分类:

                          -
                            -
                          1. 1xx:服务器接收客户端消息,但没有接收完成,等待一段时间后,发送1xx多状态码,询问是否还要继续发

                            -
                          2. -
                          3. 2xx:成功。代表:200

                            -
                          4. -
                          5. 3xx:重定向。代表:302(重定向),304(访问缓存)

                            -

                            image-20230103151112791

                            -

                            需要自动重定向到另一个C去

                            -

                            image-20230103151237984

                            -

                            发现资源未变化且本地有缓存

                            -
                          6. -
                          7. 4xx:由客户端造成的错误

                            -

                            代表:

                            -
                              -
                            1. 404(请求路径没有对应的资源,可能路径输错了)

                              -
                            2. -
                            3. 405:请求方式没有对应的doXxx方法

                              -

                              当我们在Servlet中未重写doXXX方法,就默认不能用此方法进行访问。因为doXXX方法的默认实现为:

                              -
                              String protocol = req.getProtocol();
                              String msg = lStrings.getString("http.method_get_not_supported");
                              if (protocol.endsWith("1.1")) {
                              resp.sendError(405, msg);
                              } else {
                              resp.sendError(400, msg);
                              }
                            4. -
                            -
                          8. -
                          9. 5xx:服务器端错误。

                            -

                            代表:500(服务器内部出现Exception)

                            -
                            int i = 3/0;
                          10. -
                          -
                        4. -
                        -
                      2. -
                      3. 响应头:

                        -
                          -
                        1. 格式: [头名称 : 值]

                          -
                        2. -
                        3. 常见的响应头:

                          -
                            -
                          1. Content-Type:服务器告诉客户端本次响应体 数据格式以及编码格式

                            -

                            浏览器依照编码格式来对该页面进行解码。

                            -
                          2. -
                          3. Content-disposition:服务器告诉客户端以什么格式打开响应体数据

                            -
                              -
                            • 值:
                                -
                              • in-line:默认值,在当前页面内打开
                              • -
                              • attachment;filename=xxx:以附件形式打开响应体。也即点击超链接后开始文件下载
                              • -
                              -
                            • -
                            -
                          4. -
                          -
                        4. -
                        -
                      4. -
                      5. 响应空行

                        -
                      6. -
                      7. 响应体:传输的数据

                        -
                      8. -
                      -

                      字符串格式:

                      -
                      //响应行
                      HTTP/1.1 200 OK
                      //响应头
                      Content-Type: text/html;charset=UTF-8
                      Content-Length: 101
                      Date: Wed, 06 Jun 2018 07:08:42 GMT
                      //响应空行

                      //响应体
                      <html>
                      <head>
                      <title>$Title$</title>
                      </head>
                      <body>
                      hello , response
                      </body>
                      </html>
                      +

                      以及TrieNodeWithValue::Clone()

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

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

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

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

                      +

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

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

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

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

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

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

                        +

                        Task2 concurrency

                        +

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

                        +

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

                        +

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

                        +

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

                        +

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

                        +
                        +

                        感想

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

                        +

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

                        +

                        错误集锦

                        版本1

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

                        +

                        image-20230321185244067

                        +

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

                        +

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

                        +

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

                        +

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

                        -

                        URL:统一资源定位符 : http://localhost/day14/demo1 中华人民共和国
                        URI:统一资源标识符 : /day14/demo1 共和国

                        -

                        URI的代表范围更大

                        +

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

                        -
                      10. -
                      11. 获取协议及版本 HTTP/1.1

                        -
                        String getProtocol()
                      12. -
                      13. 获取访问的客户机的IP地址

                        -
                        String getRemoteAddr()
                      14. -
                      -
                      获取请求头
                        -
                      1. 通过请求头的名称获取请求头的值

                        -
                        String getHeader(String name)
                      2. -
                      3. 获取所有的请求头名称

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

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

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

                        返回的是一个迭代器

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

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

                      +

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

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

                      这些请求头名称正是上面的键值对里的键。

                      -
                      获取请求体

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

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

                      +

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

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

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

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

                      请求体中键值对会在一行里,用&分割

                      -
                      -

                      获取时中文乱码

                      -
                        -
                      • get方式:tomcat 8 已经将get方式乱码问题解决了

                        -
                      • -
                      • post方式:会乱码

                        -
                                           * 解决:在获取参数前,设置流的编码:
                         
                        -    
                        request.setCharacterEncoding("utf-8");
                        -
                        -
                      • -
                      + +

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

                      +

                      Task3 debugging

                      感想

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

                      +

                      debug过程

                      随便贴点debug过程的截图。

                      +

                      image-20230322221526353

                      +

                      image-20230322221634506

                      +

                      image-20230322221716462

                      +

                      小问题

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

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

                      +

                      百度了下看到了这个:

                      +
                      +

                      gdb调试时好用的命令

                      +

                      image-20230323142137068

                      -
                      获取请求参数通用的方法(通用指对get和post通用)

                      这里的请求参数应该是指上面Post的请求体、Get的请求行里的参数,请求头里的参数是获取不到的。

                      -
                        -
                      1. 根据参数名称获取参数值

                        -
                        String getParameter(String name)
                        +

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

                        +

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

                        +
                        // Put a breakpoint here.

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

                        如 username=zs&password=123,getParameter(“username”)会得到zs。

                        -
                      2. -
                      3. 根据参数名称获取参数值的数组

                        -
                        String[] getParameterValues(String name)
                        - -

                        如 hobby=xx&hobby=game,会得到{xx,game}

                        -
                      4. -
                      5. 获取所有请求的参数名称

                        -
                        Enumeration<String> getParameterNames()
                      6. -
                      7. 取所有参数的map集合

                        -
                        Map<String,String[]> getParameterMap()
                      8. -
                      -
                      请求转发

                      在服务器内部资源跳转

                      -

                      image-20230102195615676

                      -

                      AServlet做了一部分事情,把剩余的事情交给BServlet去做

                      -

                      步骤:

                      -
                        -
                      1. 通过request对象获取请求转发器对象

                        -
                        RequestDispatcher getRequestDispatcher(String path)
                      2. -
                      3. 使用RequestDispatcher对象来进行转发

                        -
                        requestDispatcher.forward(ServletRequest request, ServletResponse response) 
                      4. -
                      -
                      @WebServlet("/demo2")
                      public class Servletdemo2 extends GenericServlet {

                      public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
                      System.out.println("I am "+Servletdemo2.class.getName());
                      //进行转发
                      servletRequest.getRequestDispatcher("/demo3")
                      .forward(servletRequest,servletResponse);
                      }
                      }

                      @WebServlet("/demo3")
                      public class ServletDemo3 extends GenericServlet {

                      public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
                      System.out.println("I am "+ServletDemo3.class.getName());
                      }
                      }
                      - -

                      特点:

                      -
                        -
                      1. 浏览器地址栏路径不变
                      2. -
                      3. 只能在服务器内部跳转,只能转发到服务器内部的资源中
                      4. -
                      5. 转发是一次请求,多个资源使用的是同一次请求
                      6. -
                      -
                      共享数据

                      接力工作的两个Servlet可以通过request对象进行数据通信。

                      -
                      * 方法:
                      -
                      -
                        -
                      1. 存储键值对

                        -
                        void setAttribute(String name,Object obj)
                      2. -
                      3. 获取值

                        -
                        Object getAttitude(String name)
                      4. -
                      5. 移除键值对

                        -
                        void removeAttribute(String name)
                      6. -
                      -
                      获取ServletContext
                      ServletContext getServletContext()
                      - -
                      练习:结合数据库与Servlet进行用户登录
                      -

                      要求:

                      -

                      1.编写login.html登录页面
                      username & password 两个输入框
                      2.使用Druid数据库连接池技术,操作mysql,day14数据库中user表
                      3.使用JdbcTemplate技术封装JDBC
                      4.登录成功跳转到SuccessServlet展示:登录成功!用户名,欢迎您
                      5.登录失败跳转到FailServlet展示:登录失败,用户名或密码错误

                      -
                      -
                      文件结构

                      ![屏幕截图 2023-01-02 235207](./JavaWeb/屏幕截图 2023-01-02 235207.png)

                      +

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

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

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

                      +

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

                      -

                      错误历程

                      +

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

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

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

                      +

                      Task4 SQL String Functions

                      +

                      Now it is time to dive into BusTub itself!

                      +

                      You will need to implement upper and lower SQL functions.

                      +

                      This can be done in 2 steps:

                        -
                      1. 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. implement the function logic in string_expression.h.
                      6. +
                      7. register the function in BusTub, so that the SQL framework can call your function when the user executes a SQL, in plan_func_call.cpp.
                      +

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

                      +
                      cd build
                      make -j`nproc` shell
                      ./bin/bustub-shell
                      bustub> select upper('AbCd'), lower('AbCd');
                      ABCD abcd
                      -
                      druid.properties
                      driverClassName=com.mysql.jdbc.Driver
                      url=jdbc:mysql://localhost:3306/helloworld
                      username=root
                      password=root
                      initialSize=5
                      maxActive=10
                      maxWait=3000
                      +

                      感想

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

                      +

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

                      +

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

                      +

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

                      +

                      image-20230322154915206

                      +

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

                      +

                      image-20230322154849449

                      +

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

                      +

                      代码规范

                      依旧是这三件套:

                      +
                      make format
                      make check-lint
                      make check-clang-tidy-p0
                      -
                      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>
                      +

                      错误集锦

                      关于sanitizer

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

                      +

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

                      +]]> + + + 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]*?\*/

                      +
                      +

                      第一章 简介

                      线程的作用

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

                      + + + + +

                      这个不同于上面的方法:将共享对象包装为线程安全的。它是要求了这些共享对象仅能在事件线程中运行,这样来保证线程安全性。

                      +

                      第二章 线程安全性

                      **线程安全的核心就是对状态的访问和操作进行管理**,特别是对那些共享(shared)的、可变(mutable)的状态。关于本句话,其中几点将在下面一一细说:

                        -
                      1. JavaBean:标准的Java类

                        -
                         1. 要求:
                        -     1. 类必须被public修饰
                        -     2. 必须提供空参的构造器
                        -     3. 成员变量必须使用private修饰
                        -     4. 提供公共setter和getter方法
                        - 2. 功能:封装数据
                        -
                        +
                      2. 状态

                        +

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

                      3. -
                      4. 概念:

                        -

                        ​ 成员变量:
                        ​ 属性:setter和getter方法截取后的产物

                        -
                                   例如:getUsername() --> Username--> username
                        -
                        +
                      5. 共享和可变

                        +

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

                      6. -
                      7. 方法:

                        -
                        1. setProperty()
                        -1. getProperty()
                        -1. populate(Object obj , Map map):
                        -
                        -

                        ​ 将map集合的键值对信息,封装到对应的JavaBean对象中

                        +
                      8. 是否需要线程安全

                        +

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

                      -
                      -
                      JDBCUtils

                      原封不动地照搬了:第二部分-数据库连接池-Druid-定义工具类 部分的代码。

                      -
                      UserDao
                      public class UserDao {
                      private static final JdbcTemplate jdbcTemplate = new JdbcTemplate(JDBCUtils.getDataSource());
                      public static User login(User user){
                      List<User> users = jdbcTemplate.query("select * from usr where uname = ? and pass = ?",
                      new BeanPropertyRowMapper<User>(User.class),
                      user.getUname(),user.getPass());
                      if (users.size() == 0)
                      return null;
                      else
                      return users.get(0);
                      }
                      }
                      + -

                      Response

                      功能

                      设置响应消息。

                      -
                      设置响应行

                      设置状态码

                      -
                      setStatus(int sc);
                      -
                      设置响应头
                      setHeader(String name, String value) 
                      -
                      设置响应体

                      以流的方式传输数据。

                      -

                      使用步骤:

                      -
                        -
                      1. 获取输出流

                        -
                          -
                        1. 字节输出流

                          -
                          ServletOutputStream getOutputStream()
                        2. -
                        3. 字符输出流

                          -
                          PrintWriter getWriter()
                        4. -
                        -
                      2. -
                      3. 使用输出流,将数据输出到客户端浏览器

                        -
                      4. -
                      -
                      案例
                      重定向

                      资源跳转的一种方式。

                      -

                      image-20230103153445565

                      -
                      @WebServlet("/demo1")
                      public class ServletDemo extends HttpServlet {
                      @Override
                      protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                      System.out.println("I am demo1 "+req.hashCode());
                      /* 重定向 */
                      //设置状态码
                      resp.setStatus(302);
                      //要填的是完整资源路径。
                      resp.setHeader("location","/practice_war/demo2");
                      }

                      @Override
                      protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                      doGet(req,resp);
                      }
                      }
                      +

                      什么是线程安全

                      概念

                      -
                      @WebServlet("/demo2")
                      public class ServletDemo2 extends HttpServlet {
                      @Override
                      protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                      System.out.println("I am demo2 "+req.hashCode());
                      }

                      @Override
                      protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                      doGet(req,resp);
                      }
                      }
                      + -
                      输出:
                      I am demo1 1675674230
                      I am demo2 1675674230
                      + -

                      重定向的这几行代码其实是可以简化的:

                      -
                      /* 重定向 */
                      //设置状态码
                      resp.setStatus(302);
                      //要填的是完整资源路径。
                      resp.setHeader("location","/practice_war/demo2");
                      +

                      注意,线程安全不会违背不变性和后验条件,这句话在后面会用到。

                      +

                      无状态对象一定是线程安全的

                      在此举例一个无状态线程:

                      + -

                      可以简化为:

                      -
                      resp.sendRedirect("/practice_war/demo2");
                      +
                      @ThreadSafe
                      public class StatelessFactorize implements Servlet{
                      public void service(ServletRequest req,ServletResponse resp){
                      BigInteger i = extractFromRequest(req);
                      BigInteger[] factors = factor(i);
                      encodeIntoResponse(resp,factors);
                      }
                      }
                      -
                      -

                      关于req对象不一样,但hashcode值相同的解释:

                      -

                      hashcode很大程度与对象内存空间相关,与对象的具体内容没什么关系。两个对象拥有相同的hashcode有可能只是因为存储的内存空间位置大小都相同导致的。所以是因为两次的req对象都占用了同一个内存空间【JVM调度问题】,所以才让hashcode值相同。这两个对象实质上是不一样的。

                      -
                      -

                      重定向的特点(与请求转发完全相反):

                      -
                        -
                      1. 浏览器地址栏路径改变
                      2. -
                      3. 可以访问其他站点的资源
                      4. -
                      5. 使用多次请求,不能使用request对象共享数据
                      6. -
                      -

                      路径写法:

                      -
                        -
                      1. 相对路径:通过相对路径不可以确定唯一资源

                        -
                          -
                        • 规则:找到当前资源和目标资源之间的相对位置关系
                        • -
                        -
                      2. -
                      3. 绝对路径:通过绝对路径可以确定唯一资源

                        -
                          -
                        • 如:http://localhost/day15/responseDemo2 /day15/responseDemo2

                          -
                          <form action="/webdemo4_war/check" method="post">
                        • -
                        • 以/开头的路径

                          -
                        • -
                        • 规则:判断定义的路径是给谁用的?判断请求将来从哪儿发出

                          -
                            -
                          • 客户端浏览器使用:需要加虚拟目录(项目的访问路径)

                            -

                            比如说在页面中弄了个a标签,将来是要给客户端点的,那么这个a标签的href就要用绝对路径。

                            -

                            再比如说重定向:

                            -
                            //要填的是完整资源路径。
                            resp.setHeader("location","/practice_war/demo2");
                            -

                            这个路径将来是给客户端将来要使用的路径,是客户端路径,所以要加虚拟目录。

                            - -
                          • -
                          • 服务器使用:不需要加虚拟目录

                            -

                            比如说之前的请求转发

                            -
                              -
                            • 转发路径
                            • -
                            -
                          • -
                          -
                        • -
                        -
                      4. -
                      -
                      服务器输出字符数据到浏览器
                      @WebServlet("/responseDemo4")
                      public class ResponseDemo4 extends HttpServlet {
                      protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

                      //获取流对象之前,设置流的默认编码:ISO-8859-1 设置为:GBK
                      response.setCharacterEncoding("utf-8");

                      //告诉浏览器,服务器发送的消息体数据的编码。建议浏览器使用该编码解码
                      response.setHeader("content-type","text/html;charset=utf-8");

                      /* 也有设置编码的简单形式
                      //简单的形式,设置编码
                      response.setContentType("text/html;charset=utf-8");
                      */

                      //1.获取字符输出流
                      PrintWriter pw = response.getWriter();
                      //2.输出数据
                      //pw.write("<h1>hello response</h1>");
                      pw.write("你好啊啊啊 response");
                      }

                      protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                      this.doPost(request,response);
                      }
                      }
                      -
                      服务器输出字节数据到浏览器
                      @WebServlet("/responseDemo5")
                      public class ResponseDemo5 extends HttpServlet {
                      protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                      //依然要保证编码一致
                      response.setContentType("text/html;charset=utf-8");

                      //1.获取字节输出流
                      ServletOutputStream sos = response.getOutputStream();
                      //2.输出数据
                      sos.write("你好".getBytes("utf-8"));
                      }

                      protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                      this.doPost(request,response);
                      }
                      }
                      + -
                      验证码
                      @WebServlet("/demo1")
                      public class ServletDemo extends HttpServlet {
                      @Override
                      protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                      //验证码图片大小
                      final int width = 100;
                      final int height = 50;

                      /* 绘制验证码 */
                      BufferedImage image = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
                      Graphics pen = image.getGraphics();
                      //绘制背景
                      pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
                      pen.fillRect(0,0,width,height);
                      //绘制边框
                      pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
                      pen.drawRect(0,0,width-1,height-1);
                      //随机填充字母数字
                      String source = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1234567890";
                      for (int i = 1; i <= 4; i++){
                      int index = (int)(Math.random()*source.length());
                      pen.drawString(source.substring(index,index+1),20*i,27);
                      }
                      //画干扰色线
                      pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
                      for (int i = 0; i < 5; i++){
                      pen.drawLine((int)(Math.random()*width),(int)(Math.random()*height),(int)(Math.random()*width),(int)(Math.random()*height));
                      }

                      //将图片输出
                      ImageIO.write(image,"jpg",resp.getOutputStream());
                      }

                      @Override
                      protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                      doGet(req,resp);
                      }
                      }
                      +

                      无状态对象一定是线程安全的

                      +

                      原子性

                      引例

                      我们可以在无状态对象的基础上为它增加一个域:

                      + -
                      <!DOCTYPE html>
                      <html lang="en">
                      <head>
                      <meta charset="UTF-8">
                      <title>Title</title>
                      </head>
                      <body>
                      <img id="img" src="/practice_war/demo1"/>
                      <a href="" id = "a">看不清?换一张</a>
                      <script>
                      window.onload = function (){
                      let img = document.getElementById("img");
                      let a = document.getElementById("a");
                      img.onclick = function (){
                      //加时间戳作为请求参数,为了防止浏览器不更换图片缓存
                      img.src = "/practice_war/demo1?"+new Date().getTime();
                      }
                      a.onclick = img.onclick;
                      }
                      </script>
                      </body>
                      </html>
                      +

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

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

                      ServletContext对象

                      概念

                      代表整个web应用,可以和servlet容器(服务器)通信

                      -

                      获取

                      通过request对象获取
                      ServletContext getServletContext()
                      +

                      它并不具有原子性。

                      +

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

                      +

                      竞态条件

                      -
                      通过HttpServlet获取
                      this.getContext();
                      +

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

                      +

                      读取-修改-写入

                      如上引例

                      +

                      先检查后执行

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

                      +
                      public class LazyInitRace {
                      private ExpensiveObject instance = null;

                      public ExpensiveObject getInstance() {
                      if (instance == null)
                      instance = new ExpensiveObject();
                      return instance;
                      }
                      }
                      -

                      功能

                      获取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的目录
                      */
                      +

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

                      +

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

                      +

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

                      + -

                      案例:文件下载

                      要求
                        -
                      • 文件下载需求:
                          -
                        1. 页面显示超链接
                        2. -
                        3. 点击超链接后弹出下载提示框
                        4. -
                        5. 完成图片文件下载,那种会存到你电脑download目录下,而不是直接加载出来的
                        6. -
                        -
                      • -
                      -

                      image-20230201170701909

                      -

                      用户点击下载->请求发送给某个servlet,servlet修改response->tomcat响应用户,传递的图片资源按照response的方法打开

                      -
                      代码

                      说实话看了感觉有点难以下手,主要还是完全不知道html和servlet怎么交互造成的,看了老师讲解才有点恍然大悟。

                      -

                      我们可以把a标签以重定向的角度去看。它会新建一个request,然后发送到它的href中的那个url。在此处我们将url设置为/practice_war/download?filename=1.jpg,也即要以GET的方式发送给download,请求体为filename=1.jpg。然后servlet执行结束后,就会将信息存储在resp中返回给tomcat,由tomcat发送给用户。

                      -
                      html
                      <body>
                      <a href="/practice_war/download?filename=1.jpg" id = "a">点击下载图片</a>
                      </body>
                      +

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

                      +

                      复合操作

                      -
                      servlet

                      思路:

                      -

                      获取要下载的资源,并且将其输入到resp的stream中。

                      -

                      有一点需要非常注意:

                      -
                      resp.setContentType(this.getServletContext().getMimeType(path));
                      resp.setHeader("content-disposition","attachment;filename="+name);
                      + -

                      必须要在把资源输入到resp的stream前设置好,精确来说是调用sos.write前设置好,不然无法起作用。

                      -

                      猜测是因为可能resp会根据disposition方式的不同而自动决策write的方式。

                      -
                      @Override
                      protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                      //获取要下载的资源名称
                      String name = req.getParameter("filename");

                      //获取路径
                      String path = this.getServletContext().getRealPath("/img/"+name);
                      //使用字节流
                      FileInputStream fis = new FileInputStream(path);
                      //输出数据

                      resp.setContentType(this.getServletContext().getMimeType(path));
                      resp.setHeader("content-disposition",
                      "attachment;filename="+
                      // 为了防止中文乱码,需要针对不同的浏览器来进行编码
                      DownLoadUtils.getFileName(req.getHeader("user-agent"),name));

                      //获取字节输出流
                      ServletOutputStream sos = resp.getOutputStream();
                      byte[] buff = new byte[1024];
                      int len = 0;
                      while((len = fis.read(buff))!=-1){
                      sos.write(buff,0,len);
                      }
                      //释放资源
                      fis.close();

                      // resp.setContentType(this.getServletContext().getMimeType(path));
                      // resp.setHeader("content-disposition","attachment;filename="+name);
                      }
                      -

                      会话

                      会话:一次会话中包含多次请求和响应。

                      -
                        -
                      • 一次会话:浏览器第一次给服务器资源发送请求,会话建立,直到有一方断开为止

                        -
                      • -
                      • 功能:请求之间本来是相互独立的。将多次请求组织在一次会话中,就可以让请求之间进行数据的共享。

                        -
                      • -
                      • 方式:

                        -
                          -
                        • 客户端会话技术 Cookie

                          -

                          把数据存进客户端

                          -
                        • -
                        • 服务器端会话技术 Session

                          -

                          把数据存进服务器端

                          -
                        • -
                        -
                      • -
                      -

                      概念

                      客户端会话技术,将数据保存到客户端

                      -

                      快速入门

                        -
                      • 使用步骤:

                        -
                          -
                        1. 创建Cookie对象,绑定数据【为了从服务器端发送cookie给客户端】
                            -
                          • new Cookie(String name, String value)
                          • -
                          • 可以看到,Cookie其实就是一种name-value这样的键值对对象
                          • -
                          -
                        2. -
                        3. 发送Cookie对象【因为要发送给客户端,所以应该在response里存】
                            -
                          • response.addCookie(Cookie cookie)
                          • -
                          -
                        4. -
                        5. 获取Cookie,拿到数据【因为是来自客户端,所以要从request里要】
                            -
                          • Cookie[] request.getCookies()
                          • -
                          -
                        6. -
                        -
                      • -
                      • 代码

                        -
                        @WebServlet("/demo")
                        public class ServletDemo extends HttpServlet {
                        @Override
                        protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                        response.addCookie(new Cookie("password","abc123"));
                        }

                        @Override
                        protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                        this.doGet(request, response);
                        }
                        }
                        -
                        @WebServlet("/demo2")
                        public class ServletDemo2 extends HttpServlet {
                        @Override
                        protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                        System.out.println(request.getCookies());
                        }

                        @Override
                        protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                        this.doGet(request, response);
                        }
                        }
                      • -
                      • 得到效果

                        -

                        运行服务器,首先访问/demo,然后在同一个浏览器再次访问/demo2,就可以在控制台看到输出。

                        -

                        这个过程发生了什么呢?

                        -

                        首先,访问/demo就相当于建立了会话。/demo的Servlet获取到请求之后,在response中将cookie填入。

                        -

                        保持浏览器窗口不变,会话也不变。

                        -

                        再次访问/demo2,cookie信息自动保存在request对象中。/demo2的Servlet获取到请求之后,在控制台中打印输出了cookie。

                        -
                      • -
                      -

                      细节学习

                      一次发送多个cookie

                      你看它那个API叫add,就知道数据结构差不多是个list,所以多次add就行。

                      -
                      保存时间

                      默认情况下,浏览器关闭则cookie就马上被销毁。

                      -

                      如果需要持久化存储:

                      -
                      cookie.setMaxAge(int seconds)
                      +

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

                      参数:

                      -
                        -
                      1. 正数:将Cookie数据写到硬盘的文件中。持久化存储。并指定cookie存活时间,时间到后,cookie文件自动失效
                      2. -
                      3. 负数:默认值
                      4. -
                      5. 零:删除cookie信息
                      6. -
                      -
                      中文问题

                      在tomcat 8 之前 cookie中不能直接存储中文数据,需要将中文数据转码——一般采用URL编码(%E3)

                      -

                      在tomcat 8 之后,cookie支持中文数据。

                      -
                      获取范围
                        -
                      1. 假设在一个tomcat服务器中,部署了多个web项目,那么在这些web项目中cookie能不能共享?

                        -
                          -
                        • 默认情况下cookie不能共享

                          -
                        • -
                        • 共享方法:

                          -

                          setPath(String path):设置cookie的获取范围。默认情况下,设置当前的虚拟目录

                          -

                          如果要共享,则可以将path设置为”/“

                          -
                        • -
                        -
                      2. -
                      3. 不同的tomcat服务器间cookie共享问题?

                        -

                        比如说:

                        -image-20230221225514567 -
                          -
                        • setDomain(String path):如果设置一级域名相同,那么多个服务器之间cookie可以共享

                          -

                          setDomain(".baidu.com"),那么tieba.baidu.com和news.baidu.com中cookie可以共享)

                          -
                        • -
                        -
                      4. -
                      -

                      作用和特点

                      特点:

                      -
                        -
                      1. cookie存储数据在客户端浏览器

                        -

                        因而它相对不安全

                        -
                      2. -
                      3. 浏览器对于单个cookie 的大小有限制(4kb) 以及 对同一个域名下的总cookie数量也有限制(20个)

                        -
                      4. -
                      -

                      作用:

                      -
                        -
                      1. cookie一般用于存出少量的不太敏感的数据

                        -
                      2. -
                      3. 在不登录的情况下,完成服务器对客户端的身份识别

                        -

                        比如说,以不登录情况下对某个网页进行属性设置,你下次打开的时候属性设置依然在,这是因为你的属性设置的cookie在设置后被存入到你的电脑中,下次访问该网页发出请求,服务器端就能根据请求中cookie里的属性设置信息来做出响应了。

                        -
                      4. -
                      -

                      案例:记住上一次访问时间

                      需求:
                      1. 访问一个Servlet,如果是第一次访问,则提示:您好,欢迎您首次访问。
                      2. 如果不是第一次访问,则提示:欢迎回来,您上次访问时间为:显示时间字符串

                      -
                      public class ServletDemo extends HttpServlet {
                      @Override
                      protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                      //获取流对象之前,设置流的默认编码:ISO-8859-1 设置为:GBK
                      response.setCharacterEncoding("utf-8");

                      //告诉浏览器,服务器发送的消息体数据的编码。建议浏览器使用该编码解码
                      response.setHeader("content-type","text/html;charset=utf-8");
                      if(request.getCookies() != null)
                      for(Cookie c : request.getCookies()){
                      if(c.getName().equals("isfirst")){
                      response.getWriter()
                      .write("<h1>欢迎回来,您上次访问的时间为<h1>"+c.getValue());
                      break;
                      }
                      }
                      else
                      response.getWriter().write("<h1>你好!欢迎你!<h1>");

                      SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");
                      Date date1 = new Date();
                      String currentTime = dateFormat.format(date1);

                      response.addCookie(new Cookie("isfirst",currentTime));
                      }

                      @Override
                      protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                      this.doGet(request, response);
                      }
                      }
                      -

                      Session

                      概念

                      服务器端会话技术,在一次会话的多次请求间共享数据,将数据保存在服务器端的对象中。HttpSession

                      -

                      应用场合

                      比如说购物网站的购物车这种,就会存在session。想想也是(

                      -
                        -
                      1. session用于存储一次会话的多次请求的数据,存在服务器端

                        -

                        比如说,当我们做重定向的时候,就可以选择用session共享数据(会话域)而非使用ServletContext(此范围过大)

                        -
                      2. -
                      3. session可以存储任意类型,任意大小的数据

                        -
                      4. -
                      5. session与Cookie的区别:

                        -
                          -
                        1. session存储数据在服务器端,Cookie在客户端
                        2. -
                        3. session没有数据大小限制,Cookie有
                        4. -
                        5. session数据安全,Cookie相对于不安全
                        6. -
                        -
                      6. -
                      -

                      快速入门

                        -
                      1. 获取HttpSession对象:
                        HttpSession session = request.getSession();

                        -
                      2. -
                      3. 使用HttpSession对象:

                        -
                          Object getAttribute(String name)  
                        void setAttribute(String name, Object value)
                        void removeAttribute(String name)

                        #### 原理

                        ![image-20230223102722575](./JavaWeb/image-20230223102722575.png)

                        实现依赖于Cookie

                        #### 细节

                        前面说到,当客户端和服务器端有任何一端关闭之后,会话结束,在这种情况下,session在客户端和服务器端的保留情况不同。

                        1. 当客户端关闭后,服务器不关闭,两次获取session是否为同一个?
                        * 默认情况下。不是。
                        * 如果需要相同,则可以创建Cookie,键为JSESSIONID,设置最大存活时间,让cookie持久化保存。
                        ```java
                        Cookie c = new Cookie("JSESSIONID",session.getId());
                        c.setMaxAge(60*60);
                        response.addCookie(c);
                      4. -
                      5. 客户端不关闭,服务器关闭后,两次获取的session是同一个吗?

                        -
                      6. -
                      -
                        -
                      • 不是同一个,但是要确保数据不丢失。tomcat自动(IDEA不会活化)完成以下工作
                          -
                        • session的钝化:(序列化)
                              * 在服务器正常关闭之前,将session对象序列化到硬盘上
                          -
                          -
                        • -
                        -
                      • -
                      -
                              * 具体是会放在这里:
                      +
                       
                      -      ![image-20230223104447097](./JavaWeb/image-20230223104447097.png)
                      -    
                      -* session的活化:(反序列化
                      -    * 在服务器启动后,将session文件转化为内存中的session对象即可。
                       
                      -我想,cookie应该在这点上不会像session这么做,因为cookie本质上是保存在客户端的数据,按理来说服务器端把cookie发出去之后就可以销毁了,在服务器序列化一点意义都没有。
                      -
                      -
                        -
                      1. 销毁时间

                        -
                          -
                        1. 服务器关闭

                          -
                        2. -
                        3. session对象调用invalidate() 。

                          -
                        4. -
                        5. session默认失效时间 30分钟
                          选择性配置修改

                          -

                          可以在每个项目的子配置文件(如下图)或者总的项目的父配置文件apache-tomcat-8.5.83\conf\web.xml中配置

                          -

                          image-20230223105053170

                          -
                          <session-config>
                          <session-timeout>30</session-timeout>
                          </session-config>
                        6. -
                        -
                      2. -
                      -

                      案例

                      -

                      需求:

                      -
                        -
                      1. 访问带有验证码的登录页面login.jsp
                      2. -
                      3. 用户输入用户名,密码以及验证码。
                          -
                        • 如果用户名和密码输入有误,跳转登录页面,提示:用户名或密码错误
                        • -
                        • 如果验证码输入有误,跳转登录页面,提示:验证码错误
                        • -
                        • 如果全部输入正确,则跳转到主页success.jsp,显示:用户名,欢迎您
                        • -
                        -
                      4. -
                      -
                      -
                      初见思路

                      我们可以在服务器端使用session存储password和username的map,存储验证码图片编号和图片的map,然后用cookie携带验证码图片编号,在req中存储用户名和密码。

                      -
                      正确思路

                      感觉我上面的思路是没有充分利用到session的性质,仅仅把它作为在服务器端存储数据的工具,

                      -

                      “在服务器端存储password和username的map,存储验证码图片编号和图片的map,然后用cookie携带验证码图片编号,在req中存储用户名和密码。”

                      -

                      这样也依然成立,跟session没半毛钱关系。我们可以这样使用session:

                      -
                        -
                      1. 在服务器端存储password和username的map,存储验证码图片编号和图片的map
                      2. -
                      3. 当会话建立,由于没有cookie,故而session第一次创建。我们在session内写入验证码对应的编号,把图片通过response发送给客户端。
                      4. -
                      5. 会话端输入图片验证码后,按下submit按键,验证码存入request域,向服务器端发送请求
                      6. -
                      7. 服务器端Servlet从请求中get到验证码,然后在session中get到当前验证码的图片编号,向一开始存储的map查询数据,这样就能验证验证码是否正确了
                      8. -
                      -

                      那么在这里为什么不用Cookie而使用session呢?大概是因为cookie不安全罢(慌乱)

                      -
                      代码
                      jsp
                      <html lang="en">
                      <head>
                      <meta charset="UTF-8">
                      <title>Title</title>
                      </head>
                      <body>
                      <form action="/practice_war/loginServlet" method="post">
                      name: <input type="text" name="username" id="username" placeholder="请输入用户名">
                      password: <input type="password" name="password" id="password" placeholder="请输入密码">
                      verifycode:<input type="text" name="verifycode" id="verifycode" placeholder="请输入验证码">

                      <img id="img" src="/practice_war/check"/>

                      <input type="submit" value="submit">
                      </form>
                      <script>
                      window.onload = function(){
                      document.getElementById("img").onclick = function(){
                      this.src = "/practice_war/check?"+new Date().getTime();
                      }
                      }

                      </script>
                      </body>
                      </html>
                      -
                      checkcode
                      @WebServlet("/check")
                      public class ServletDemo extends HttpServlet {
                      @Override
                      protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                      //验证码图片大小
                      final int width = 100;
                      final int height = 50;

                      /* 绘制验证码 */
                      BufferedImage image = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
                      Graphics pen = image.getGraphics();
                      //绘制背景
                      pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
                      pen.fillRect(0,0,width,height);
                      //绘制边框
                      pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
                      pen.drawRect(0,0,width-1,height-1);
                      //随机填充字母数字
                      String source = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1234567890";

                      StringBuilder verifyAnswer = new StringBuilder();

                      for (int i = 1; i <= 4; i++){
                      int index = (int)(Math.random()*source.length());
                      verifyAnswer = verifyAnswer.append(source.charAt(index));
                      pen.drawString(source.substring(index,index+1),20*i,27);
                      }
                      //画干扰色线
                      pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
                      for (int i = 0; i < 5; i++){
                      pen.drawLine((int)(Math.random()*width),(int)(Math.random()*height),(int)(Math.random()*width),(int)(Math.random()*height));
                      }

                      request.getSession().setAttribute("verifycode",verifyAnswer.toString());
                      System.out.println("verify:"+verifyAnswer.toString());
                      //将图片输出
                      ImageIO.write(image,"jpg",response.getOutputStream());
                      }

                      @Override
                      protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                      this.doGet(request, response);
                      }
                      }
                      +

                      加锁机制

                      线程安全分析法与为什么要加锁

                      上面说到,当对象内仅有一个状态时,可以通过使用线程安全类来保障原子性。但当对象里存在多个状态时,就必须用锁来进行线程同步,而非简单地用多个线程安全类。

                      +

                      还是以上面的实例来解释。

                      + -
                      login
                      @WebServlet("/loginServlet")
                      public class loginServlet extends HttpServlet {
                      @Override
                      protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                      HttpSession session = request.getSession();
                      String verifycode = request.getParameter("verifycode");
                      System.out.println(flag);

                      String ans = session.getAttribute("verifycode");
                      if (ans == null||!ans.equals(verifycode)){
                      session.removeAttribute("verifycode");
                      // 重定向到错误界面
                      request.getRequestDispatcher("/fail_code").forward(request,response);
                      return;
                      }
                      session.removeAttribute("verifycode");

                      // 进行密码账号匹配处理
                      String username = request.getParameter("username");
                      String password = request.getParameter("password");
                      System.out.println(username+" "+password);
                      if(UserDao.login(new User(username,password))){
                      // 成功界面
                      request.setAttribute("uname",username);
                      request.getRequestDispatcher("/success").forward(request,response);
                      }else{
                      // 失败界面
                      request.getRequestDispatcher("/fail").forward(request,response);
                      }
                      }

                      @Override
                      protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                      this.doGet(request, response);
                      }
                      }
                      +
                      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);
                      }
                      }
                      }
                      -

                      老师的写法是将错误信息直接写在原登录界面,和我的略有不同:

                      -
                      // 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 这种耦合杂乱的页面代码,但是模板引擎的思路大致相同,还是可以看一看的

                      -
                      -

                      改动之后无需重启服务器,刷新界面即可。

                      +

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

                      + + + + +

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

                      +

                      内置锁

                      + + + +

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

                      -

                      关于热更新的机制可以看看这篇文章,水平有限还看不懂就先放在这了:

                      -

                      JSP热部署的实现原理[通俗易懂]

                      +

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

                      -

                      概念

                      JSP(Java Server Pages) Java服务器端页面,用于简化书写

                      -

                      可以理解为:一个特殊的页面,其中既可以定义html标签,又可以定义java代码

                      -

                      比如说,上一个案例的Servlet代码就可以直接写入到JSP中,而且response和request这些对象可以直接用

                      -
                      <%@ page import="java.text.SimpleDateFormat" %>
                      <%@ page import="java.util.Date" %>
                      <html>
                      <body>
                      <h2>Hello World!</h2>
                      <%
                      //获取流对象之前,设置流的默认编码:ISO-8859-1 设置为:GBK
                      response.setCharacterEncoding("utf-8");

                      //告诉浏览器,服务器发送的消息体数据的编码。建议浏览器使用该编码解码
                      response.setHeader("content-type","text/html;charset=utf-8");
                      if(request.getCookies() != null)
                      for(Cookie c : request.getCookies()){
                      if(c.getName().equals("isfirst")){
                      //response.getWriter().write("<h1>欢迎回来,您上次访问的时间为<h1>"+c.getValue());
                      response.getWriter().write("<h1>Welcome!The last time you visit is <h1>"+c.getValue());
                      //System.out.println("欢迎回来,您上次访问的时间为"+c.getValue());
                      break;
                      }
                      }
                      else
                      response.getWriter().write("<h1>Hello!Welcome!<h1>");

                      SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");
                      Date date1 = new Date();
                      String currentTime = dateFormat.format(date1);

                      //response.getWriter().write("<h1>你好!欢迎你!<h1>");
                      //System.out.println("你好!欢迎你!");
                      response.addCookie(new Cookie("isfirst",currentTime));
                      %>
                      </body>
                      </html>
                      + -

                      最终效果:

                      -image-20230222230929893 +

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

                      + -

                      原理

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

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

                      +

                      重入

                      -

                      用来配置jsp的资源页面信息

                      -
                        -
                      • 分类:
                          -
                        1. page : 配置JSP页面的
                            -
                          • contentType:等同于response.setContentType()
                            contentType="text/html;charset=UTF-8"
                            -
                              -
                            1. 设置响应体的mime类型以及字符集
                            2. -
                            3. 设置当前jsp页面的编码(只能是高级的IDE才能生效,如果使用低级工具,则需要设置pageEncoding属性设置当前页面的字符集)
                            4. -
                            -
                          • -
                          • import:导包
                          • -
                          • errorPage:当前页面发生异常后,会自动跳转到指定的错误页面
                          • -
                          • isErrorPage:标识当前页面是否是错误页面。
                              -
                            • true:是,可以使用内置对象exception【用来获取异常名称/信息等】
                            • -
                            • false:否。默认值。不可以使用内置对象exception
                            • -
                            -
                          • -
                          -
                        2. -
                        3. include : 页面包含的。导入页面的资源文件,可以引入其它的jsp或者html,引入之后就会展示同样的内容。
                            -
                          • <%@include file=”top.jsp”%>
                          • -
                          -
                        4. -
                        5. taglib导入资源。用来导入标签库
                              * <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
                          -        * prefix:前缀,自定义的。之后就可以用`<c:XXX>`了。相当于什么std::。
                          -
                          -
                        6. -
                        -
                      • -
                      -

                      中文乱码

                      但是注意一点

                      -
                      //获取流对象之前,设置流的默认编码:ISO-8859-1 设置为:GBK
                      response.setCharacterEncoding("utf-8");

                      //告诉浏览器,服务器发送的消息体数据的编码。建议浏览器使用该编码解码
                      response.setHeader("content-type","text/html;charset=utf-8");
                      +

                      其中关于粒度的理解:

                      +

                      不是“每一次调用获取一次锁,该锁属于该此调用”,而是“每个线程调用时获取一次锁,该锁属于该线程”

                      + -

                      这样做在Servlet不会导致中文乱码,但JSP不行,这个大概是因为两者原理不一样。

                      -

                      Servlet的中文乱码:

                      -image-20230222231756277 -

                      JSP的:

                      -

                      image-20230222231820358

                      -

                      Servlet乱码是因为客户端和response请求体编码不一致,JSP乱码与JSP的原理有关,是只跟服务器端有关

                      -
                      -

                      编译jsp有以下几个步骤:
                      (1)把jsp转化为java源码。pageEncoding=xxx指定以xxx编码格式读取jsp文件,因此,jsp文件的编码格式应与pageEncoding值一致。
                      (2)把java源码编译为字节码,即.class文件。转化后的java源码为utf-8编码格式,字节码也为utf-8编码,我们无需干预。
                      (3)执行.class文件。在此过程,需向浏览器发送中文字符,contentType=xxx指定了jsp以xxx编码显示字符。也就是在浏览器中查看页面编码,其值为contentType指定的编码。

                      -

                      因此,在1、3环节,**只要指定一致的编码格式(jsp文件编码格式=pageEncoding=contentType)**,即可保证jsp页面不出现乱码。
                      举例:jsp文件以utf-8格式编写,那么pageEncoding=utf-8, contentType=utf-8,就保证了jsp页面不出现乱码。
                      ————————————————
                      版权声明:本文为CSDN博主「liuhaidl」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
                      原文链接:https://blog.csdn.net/liuhaidl/article/details/84012372

                      -
                      -

                      指定方法是在JSP开头添加:

                      -
                      <%@ page pageEncoding="UTF-8"%>
                      -

                      内置对象

                      在jsp页面中不需要获取和创建,可以直接使用的对象。

                      -

                      jsp一共有9个内置对象。

                      -
                        -
                      1. request HttpServletRequest 一次请求访问的多个资源(转发)

                        -
                      2. -
                      3. response HttpServletResponse 响应对象

                        -
                      4. -
                      5. out: JspWriter 字符输出流对象。可以将数据输出到页面上。和response.getWriter()类似

                        -
                          -
                        • response.getWriter()和out.write()的区别: 在tomcat服务器真正给客户端做出响应之前,会先找response缓冲区数据,再找out缓冲区数据。因而response.getWriter()数据输出永远在out.write()之前 所以说,用out更好,因为它跟随你布局变化,你out写在哪,这句话最终就会输出在哪
                        • -
                        -
                      6. -
                      7. pageContext PageContext 当前页面共享数据,还可以获取其他八个内置对象

                        -
                      8. -
                      9. session HttpSession 一次会话的多个请求间

                        -
                      10. -
                      11. application ServletContext 所有用户间共享数据

                        -
                      12. -
                      13. page Object 当前页面(Servlet)的对象,相当于this

                        -
                      14. -
                      15. config ServletConfig Servlet的配置对象

                        -
                      16. -
                      17. exception Throwable 异常对象。只在page指令的isErrorPage为true的情况下才能使用此对象。

                        -
                      18. -
                      -

                      其中,

                      -

                      image-20230307144539506

                      -

                      这四个为用来共享数据的域对象

                      -

                      演变:MVC开发模式

                      jsp的演变

                      image-20230307145442383

                      -

                      MVC模式

                      将程序分成三个部分,分别是M-V-C。

                      -
                        -
                      1. M:Model,模型。JavaBean
                          -
                        • 完成具体的业务操作,如:查询数据库,封装对象
                        • -
                        -
                      2. -
                      3. V:View,视图。JSP
                          -
                        • 展示数据
                        • -
                        -
                      4. -
                      5. C:Controller,控制器。Servlet
                          -
                        • 获取用户的输入
                        • -
                        • 调用模型
                        • -
                        • 将数据交给视图进行展示【域对象共享数据】
                        • -
                        -
                      6. -
                      -

                      image-20230307150845273

                      -

                      服务器将接收的请求给控制器处理,控制器控制model完成必要的运算,model把算出的东西返回给控制器,控制器再把数据交给视图展示,数据最终就回到了浏览器客户端。

                      -

                      这就算是一个微型CPU了吧,控制器就是CU,模型就是ALU,也许客户端和视图什么的可以视为IO接口。

                      -
                        -
                      • 优缺点:

                        -
                          -
                        1. 优点:

                          -
                            -
                          1. 耦合性低,方便维护,可以利于分工协作
                          2. -
                          3. 重用性高
                          4. -
                          -
                        2. -
                        3. 缺点:

                          -
                            -
                          1. 使得项目架构变得复杂,对开发人员要求高
                          2. -
                          -
                        4. -
                        -
                      • -
                      -

                      那么,我们可以知道,jsp就只需负责数据的展示了。那怎么展示数据呢?这就需要用到jsp的几个技术了:

                      -

                      EL表达式

                      -

                      注意,servlet3.0以来默认关闭el表达式解析,需要手动在page上加属性打开,详见 jsp文件中的el表达式失效问题解决

                      -
                      -

                      Expression language,替换和简化jsp上java代码的书写

                      -

                      语法:${表达式}

                      -

                      jsp会执行里面的表达式,然后把结果输出。

                      -

                      image-20230307151706211

                      -

                      加反斜杠可忽略。

                      -

                      使用场景:

                      -
                        -
                      1. 运算

                        -
                              1. 算数运算符: + - *  / %
                        -      2. 比较运算符: > < >= <= == !=
                        -      3. 逻辑运算符: && || !
                        -      4. 空运算符: empty
                        -   * 功能:用于判断字符串、集合、数组对象是否为null**或者**长度是否为0
                        -   * `${empty 变量名}`: 判断字符串、集合、数组对象是否为null或者长度为0
                        -   * `${not empty 变量名}`: 表示判断字符串、集合、数组对象是否不为null 并且 长度>0
                        -
                        -
                      2. -
                      3. 获取值

                        -
                          -
                        1. el表达式只能从域对象中获取值

                          -

                          image-20230307144539506

                          -
                        2. -
                        3. 语法:

                          -
                            -
                          1. ${域名称.键名}:从指定域中获取指定键的值
                          2. -
                          -
                            -
                          • 域名称:
                              -
                            1. pageScope –> pageContext
                            2. -
                            3. requestScope –> request
                            4. -
                            5. sessionScope –> session
                            6. -
                            7. applicationScope –> application(ServletContext)
                            8. -
                            -
                          • -
                          • 举例:在request域中存储了name=张三,获取:${requestScope.name}
                          • -
                          -
                            -
                          1. ${键名}:表示依次从最小的域中查找是否有该键对应的值,直到找到为止。
                          2. -
                          -
                        4. -
                        5. 案例

                          -

                          这样一来,访问/demo就能转发到index.jsp,显示出属性值

                          -
                            -
                          1. Servlet

                            -
                            protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                            request.setAttribute("name","xiunian");
                            request.getRequestDispatcher("/index.jsp").forward(request,response);
                            }
                          2. -
                          3. index.jsp

                            -
                            <%@ page pageEncoding="UTF-8" isELIgnored="false" %>
                            <html>
                            <body>
                            <h2>Hello World!</h2>
                            ${requestScope.name}
                            </body>
                            </html>
                          4. -
                          -
                        6. -
                        7. 获取非字符串类型的值

                          -
                            -
                          1. 对象

                            -
                          2. -
                          3. 集合(List、Map等)

                            -
                          4. -
                          -
                        8. -
                        9. -
                        -
                      4. -
                      -]]>
                      - - Java - -
                      - - 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]*?\*/

                      - -

                      第一章 简介

                      线程的作用

                      + -

                      这一段写得很好,非常易懂地概括了什么是“多线程把异步转化为同步”:把异步中的不同操作分解为一个个独立的同类型操作,然后只需实现这些相较简单的同类型操作,再异步地把它们调度起来就行。线程正是把复杂的异步工作流分解成了一组简单的同步工作流

                      -

                      线程无处不在

                      如果一个模块在代码中引入了并发性,那么它所有的代码路径【调用链】都得是并发的。

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

                      +

                      用锁来保护状态

                      - + - +

                      但这很考验人的记性。一旦你在某个地方忘了同步了就会寄。

                      + - -

                      这个不同于上面的方法:将共享对象包装为线程安全的。它是要求了这些共享对象仅能在事件线程中运行,这样来保证线程安全性。

                      -

                      第二章 线程安全性

                      **线程安全的核心就是对状态的访问和操作进行管理**,特别是对那些共享(shared)的、可变(mutable)的状态。关于本句话,其中几点将在下面一一细说:

                      -
                        -
                      1. 状态

                        -

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

                        -
                      2. -
                      3. 共享和可变

                        -

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

                        -
                      4. -
                      5. 是否需要线程安全

                        -

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

                        -
                      6. -
                      - +

                      活跃性与性能

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

                      什么是线程安全

                      概念

                      - - - - - -

                      注意,线程安全不会违背不变性和后验条件,这句话在后面会用到。

                      -

                      无状态对象一定是线程安全的

                      在此举例一个无状态线程:

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

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

                      - +

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

                      + @@ -3417,3275 +2295,3354 @@ url访问填写http://localhost/webdemo4_war/*.do
                    8. - 计算机体系结构 - /2024/01/04/arch/ - 01 基本知识
                        -
                      1. SISD、SIMD、MIMD、向量处理器的基本概念

                        -

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

                        -
                      2. -
                      3. 发射与流出

                        -

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

                        + Project2 B+Tree + /2023/03/13/cmu15445$lab2/ + +

                        参考

                        +

                        CMU 15-445 Project 2 (Spring 2023) | 关于 B+Tree 的十个问题

                        +

                        对crabbing lock、乐观锁做了详尽解释

                        + +

                        Project2 B+Tree

                        +

                        In this programming project you will implement a B+Tree index in your database system.

                        +

                        Your implementation will support thread-safe search, insertion, deletion (including splitting and merging nodes包括分裂和合并结点), and an iterator to support in-order leaf scans.

                        +
                        +

                        它这里的B+树(以及wiki里的)跟王道考研讲得不大一样。王道考研的B+每个结点一个关键字对应一个child,但是这里的是B树的形式。

                        +

                        undefined

                        +

                        image-20230417181239196

                        +

                        Task1 B+Tree Pages

                        +

                        You must implement three Page classes to store the data of your B+Tree:

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

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

                        +
                      4. B+Tree Page BPlusTreePage

                        +

                        下面那两个的基类

                      5. -
                      6. tensor 张量

                        -

                        sparse tensor 稀疏张量

                        +
                      7. B+Tree Internal Page

                        +

                        An Internal Page stores m ordered keys and m+1 child pointers (as page_ids) to other B+Tree Pages.These keys and pointers are internally represented as an array of key/page_id pairs.

                        +

                        Because the number of pointers does not equal the number of keys, the first key is set to be invalid, and lookups should always start with the second key.

                        +

                        At any time, each internal page should be at least half full.【min_size<= <=max_size】

                        +

                        During deletion, two half-full pages can be merged, or keys and pointers can be redistributed to avoid merging. During insertion, one full page can be split into two, or keys and pointers can be redistributed to avoid splitting.

                      8. -
                      9. 异构计算

                        -

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

                        -

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

                        +
                      10. B+Tree Leaf Page

                        +

                        The Leaf Page stores m ordered keys and their m corresponding values. In your implementation, the value should always be the 64-bit record_id for where the actual tuples are stored; see the RID class, in src/include/common/rid.h.

                        +

                        *Note:* Even though Leaf Pages and Internal Pages contain the same type of key, they may have different value types. Thus, the max_size can be different.

                      -

                      02 现代处理器体系结构

                      img

                      -

                      img

                      -

                      例题

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

                      1

                      img

                      -

                      注意几点:

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

                      2

                      img

                      -

                      img

                      -

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

                      -

                      img

                      -

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

                      -

                      题型2 换名/消除WAR WAW

                      1

                      img

                      -

                      2

                      img

                      -

                      题型3 记分牌

                      img

                      -

                      这里的结构相关值得注意

                      -

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

                      -
                        -
                      1. 指令状态表

                        -
                          -
                        1. 流出

                          -

                          无结构冲突、无WAW冲突

                          -

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

                          -

                          ② 由①可知全部流出

                          + +

                          这个没什么好说,大概就是有一个基类结点,它有两个子类,一个表示b+树的leaf node,另一个表示b+树的internal node,每个结点都占据一个内存页。嗯写就是了。

                          +

                          Task2a Insertion and Search + Task3 Iterator

                          +

                          The index should support only unique keys; if you try to reinsert an existing key into the index, it should not perform the insertion, and should return false. key必须unique

                          +

                          B+Tree pages should be split (or keys should be redistributed) if an insertion would violate the B+Tree’s invariants. 插入时需要分裂

                          +

                          If an insertion changes the page ID of the root, you must update the root_page_id in the B+Tree index’s header page. You can do this by accessing the header_page_id_ page, which is given to you in the constructor. Then, by using reinterpret_cast, you can interpret this page as a BPlusTreeHeaderPage (from src/include/storage/page/b_plus_tree_header_page.h) and update the root page ID from there. You also must implement GetRootPageId, which currently returns 0 by default.对root_page_id的一切访问,都需要通过header_page_id_。如果插入后改变了root的page ID,需要更新root_page_id

                          +

                          We recommend that you use the page guard classes from Project 1 to help prevent synchronization problems. For this checkpoint, we recommend that you use FetchPageBasic (defined in src/include/storage/page/) when you access a page. 在当前task中,我们推荐你使用pro1实现的page guard,比如说这里如果要访问一页,就需要用 FetchPageBasic

                          +

                          You may optionally use the Context class (defined in src/include/storage/index/b_plus_tree.h) to track the pages that you’ve read or written (via the read_set_ and write_set_ fields) or to store other metadata that you need to pass into other functions recursively.你可以随意使用和修改 Context class,它大概就是一个存储共享信息的对象。

                          +

                          If you are using the Context class, here are some tips:如果你要用,要注意以下几点:

                          +
                            +
                          • You might only need to use write_set_ when inserting or deleting. 当你在为B+树插入/删除结点时,需要用到write_set_。【为什么?这个set存储的是修改路径上的结点吗?然后如果要分裂/合并结点,只需什么while(pop且需要分裂/合并){分裂/合并}??所以说这里的deque是栈结构?】

                            +

                            也就是说,其实我们就可以不用递归了,而是将上下文存储在context->write_set_这个栈里面就行了?大概是这个意思吧

                            +

                            It is possible that you don’t need to use read_set_, depending on your implementation.

                            +

                            read可以用递归(比较简单)也可以不用,所以说具体看实现。

                          • -
                          • 读操作数

                            -

                            操作数可用时完成该阶段

                            -

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

                            -

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

                            +
                          • You might want to store the root page id in the context and acquire write guard of header page when modifying the B+Tree.你需要将root page id存储在context,并且在修改b+树(插入、删除)时获取header page的WritePageGurad。

                          • -
                          • 执行

                            -

                            纯纯的算术

                            -

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

                            -

                            ② 全部都结了

                            +
                          • To find a parent to the current node, look at the back of write_set_. It should contain all nodes along the access path.如果想要寻找当前node的父亲,可以看看write_set_.back,它包含了访问路径上所有结点的引用【所以确实是当成栈来用了】

                          • -
                          • 写结果

                            -

                            不存在WAR则写入

                            -

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

                            -

                            ② 除了DIV全部结了

                            +
                          • You should use BUSTUB_ASSERT to help you find inconsistent data in your implementation. 需要使用 BUSTUB_ASSERT

                            +

                            For example, if you want to split a node (except root), you might want to ensure there is still at least one node in the write_set_. If you need to split root, you might want to check if header_page_ is std::nullopt.

                            +

                            如果你想要分割一个根节点以外的node,那你必须保证write_set_中至少有一个结点;如果你想要分割根节点,那你必须保证header_page_非空。

                          • -
                        +
                      2. To unlock the header page, simply set header_page_ to std::nullopt. To unlock other pages, pop from the write_set_ and drop.如果你想要不锁住header page,那就置其为空指针;如果想释放别的页,那就将它从 write_set_ pop出来就行。【这是因为我们要用到的page类型都是page guard,可以析构时unpin吗?】

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

                        -

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

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

                        -

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

                        -
                      7. -
                      -

                      img

                      -

                      img

                      -

                      题型4 Tomasulo算法

                      3段流水

                      + +

                      感想

                      由于各种原因,lab2的战线还是拉得太长了。四月份完成了代码初版,中间修了几个bug勉强通过了insertion test,然后一直到十一月底的现在才再次捡起来。不得不说,回看当初的代码,还是能够很清晰地感受到自己这半年多来的成长的,令人感慨。

                      +

                      我先是花了一天的时间重构了下以前写的所有代码,然后再花了两天时间修bug终于通过了insertion test和sequence scale test,并且将b+树的代码修到了我满意的地步(指不像以前那样一坨重复代码和中文注释。。。)。

                      +

                      思路

                      这里简要介绍下B+树的插入实现及我觉得实现中需要注意的几个要点吧。

                      +

                      B+树的插入流程大概是这样的:

                        -
                      1. 流出

                        +
                      2. 查找到key要插入的叶子结点(途中需要维护write_set,也即查找路径)

                        +
                      3. +
                      4. 判断结点是否满

                          -
                        1. 没有结构冲突就流出,填进保留站

                          -

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

                          +
                        2. 未满,直接插入即可。(我采取插入排序的方法)

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

                          +
                        5. 已满,需要对结点进行分裂。

                          +

                          推举出中间结点tmp_key,它和新结点page_id接下来将插入到父节点中。

                        -

                        保留站有以下字段:

                        -
                          -
                        • Op:操作

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

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

                          -

                          load的Vk保存偏移量

                          -
                        • -
                        • Busy

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

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

                          -

                          存放要写入它的保留站ID

                          -
                        • -
                      5. -
                      6. 执行

                        -

                        两个操作数就绪后就执行

                        -
                      7. -
                      8. 写结果

                        -

                        计算完毕后由CDB传送

                        -
                      9. -
                      -

                      例题

                      img

                      -

                      img

                      -

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

                      -

                      img

                      -

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

                      -

                      img

                      -

                      img

                      -

                      img

                      -

                      img

                      -

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

                      -

                      img

                      -

                      题型5 Tomasulo+前瞻执行

                      4段流水

                      +
                    9. 持续进行分裂:

                      +

                      需要注意具体的分裂方法,我认为其中internal page size == 3的情况尤为棘手。在具体实现中,我是这样分裂的:

                        -
                      1. 流出

                        +
                      2. 推举出将要被插入到父节点的tmp_key

                        +

                        该推举出的key将不会出现在分裂后的新旧结点中,而是会被加入到父节点中。默认为(m + 1) / 2【m为max size】。

                        +

                        但是要尤其注意size为3的case,此时tmp_key为array_[2],很有可能右边结点为空。所以我们需要做点特殊处理:

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

                          -

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

                          +
                        2. 当要插入到该节点的insert_key > array_[(m + 1) / 2]时,我们推举(m + 1) / 2这个结点。
                        3. +
                        4. insert_key < array_[m / 2],我们转而推举m / 2(此时为array_[1])。
                        5. +
                        6. insert_key < array_[(m + 1) / 2]insert_key > array_[m / 2]时,我们应该对此做出特殊处理,推举insert_key。在此为了代码实现方便,我们还需要调换insert_key和tmp_key的地位
                        7. +
                        +
                        special = false;
                        middle = (m + 1) / 2;
                        tmp_key = root->KeyAt(middle);
                        insert_small_than_tmp_key = (comparator_(insert_key, tmp_key) < 0);
                        if (insert_small_than_tmp_key) {
                        middle = m / 2;
                        tmp_key = root->KeyAt(middle);
                        if (comparator_(insert_key, tmp_key) >= 0) {
                        special = true;
                        swap = insert_key;
                        insert_key = tmp_key;
                        tmp_key = swap;
                        }
                        }
                      3. +
                      4. 分裂旧结点

                        +

                        被推举出的tmp_key的value及其右部元素会变成新结点,左部依然留在旧结点,tmp_key会到父节点中去。也即如下图所示:

                        +

                        ![未命名文件 (1)](./cmu15445/未命名文件 (1).png)

                        +

                        依然是注意上面那个case3特殊情况,需要交换insert key和middle key:

                        +
                        if (!special)
                        new_page->SetValueAt(0, root->ValueAt(middle));
                        else {
                        new_page->SetValueAt(0, insert_val);
                        insert_val = root->ValueAt(middle);
                        }
                      5. +
                      6. 持续进行推举和分裂,直到父节点不用分裂

                        +

                        此时直接将insert key和insert value插入排序到父节点即可。

                      7. -
                      8. 具体填什么看操作数有没有就绪

                        +
                    -

                    保留站有以下字段:

                    -
                      -
                    • Op:操作

                      +

                      然后是Iterator的话,我感觉这也是设计得很不错,让我们亲手写了下c++的重载运算符,也是让我学到了很多c++知识。。。

                      +

                      遇到的问题

                      感觉问题其实不多,主要还是debug有点痛苦花了很长时间()

                      +

                      cmake报错

                      切换内核前后报错。

                      +

                      Check for working C compiler: /usr/bin/cc - broken

                      +

                      感觉可能是内核切来切去,导致cmake cache发生了点小问题?总之我最后在5.11内核把build文件删了,重新执行cmake -DCMAKE_CXX_COMPILER=$(which g++) -DCMAKE_C_COMPILER=$(which gcc) ..就ok了。

                      +

                      page guard

                      用错了

                      image-20230505002652748

                      +

                      我发现在这里创建的root最后好像会被释放掉?

                      +

                      比如我看到新root的page为6,连接也做得好好的,最后出了函数就寄了:

                      +

                      image-20230505002731312

                      +

                      还有一个是发现新的leaf page好像不大对,其类型甚至是internal呃呃,我调下看看

                      +

                      尼玛,绷不住了是这里:

                      +

                      image-20230505011731797

                      +

                      原来写的

                      +

                      image-20230505011744924

                      +

                      改了之后test2马上ok,乐

                      +
                      作用域

                      还弄了个commit修:

                      +

                      image-20231130222702358

                      +

                      一点c++引用震撼

                      auto INDEXITERATOR_TYPE::operator*() -> const MappingType &
                      + +

                      这个函数卡了我还挺久。。。里面逻辑很简单,不过难就难在怎么构造出一个const MappingType &

                      +

                      如果这样:

                      +
                      INDEX_TEMPLATE_ARGUMENTS
                      auto INDEXITERATOR_TYPE::operator*() -> const MappingType & {
                      auto page = guard_.As<LeafPage>();
                      return std::pair<KeyType, ValueType>(page->KeyAt(cnt_), page->ValueAt(cnt_));
                      // or use make_pair. the same result
                      }
                      + +

                      会说你临时对象不能作为引用。如果这样:

                      +
                      INDEX_TEMPLATE_ARGUMENTS
                      auto INDEXITERATOR_TYPE::operator*() -> const MappingType & {
                      auto page = guard_.As<LeafPage>();
                      auto res = new MappingType(std::pair<KeyType, ValueType>(page->KeyAt(cnt_), page->ValueAt(cnt_)));
                      return *res;
                      }
                      + +

                      又会找不到机会delete导致内存泄漏。冥思苦想了半天不知道该怎么办,最后从网上看了别人怎么写的:

                      +
                      INDEX_TEMPLATE_ARGUMENTS
                      auto INDEXITERATOR_TYPE::operator*() -> const MappingType & {
                      auto page = guard_.As<LeafPage>();
                      return page->PairAt(cnt_);
                      }

                      INDEX_TEMPLATE_ARGUMENTS
                      auto B_PLUS_TREE_LEAF_PAGE_TYPE::PairAt(int index) const -> const MappingType & {
                      return array_[index];
                      }
                      + +

                      我服了。

                      +

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

                      +

                      Task4 Remove

                      感想

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

                      +

                      思路

                        +
                      1. 找到需要操作的叶结点路径

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

                        +
                      4. 判断叶子结点属于以下四种策略中的哪一种,执行对应策略(优先级从高到低):

                        +
                          +
                        1. 直接删除

                          +

                          当删除后叶结点元素数仍在合法范围,并且路径上父节点没有target key,直接删除然后返回即可。

                        2. -
                        3. Vj,Vk:源操作数值

                          -

                          load的Vk保存偏移量

                          +
                        4. 更新父节点路径

                          +

                          当删除后叶结点元素数仍在合法范围,并且路径上父节点target key,直接删除然后向上回溯更新父节点即可。

                        5. -
                        6. Busy

                          +
                        7. 窃取兄弟元素

                          +
                          If do a steal, we should update related key in the parent, and update up till reaching the root.

                          /*
                          For that steal is more simple, we first check whether it can do a steal first.
                          We steal the node whose size is biggest between the next and the prev node.
                          If the prev size is bigger, we only update self key in parent.
                          If the next size is bigger, we update both self key and next key n parent.
                          After that, we trace back and update all the parent nodes which contains the
                          target key.
                          */
                          + +

                          当删除后叶结点元素数过少,并且左右兄弟元素充足,则从左右兄弟窃取一个。优先窃取元素最多者。

                          +
                            +
                          1. 窃取左兄弟

                            +

                            窃取左兄弟的最大元素

                            +

                            需递归更新自身父节点路径上的对应值。

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

                            +
                          4. 窃取右兄弟

                            +

                            窃取右兄弟的最小元素

                            +

                            需要递归更新自身和右兄弟父节点路径上的对应值。

                          5. -
                          6. Qi:寄存器状态表

                            -

                            存放要写入它的保留站ID

                            +
                          +

                          之后返回即可。

                        8. -
                    +
                  5. 合并

                    +
                    /*
                    Need to merge with one of the node. It is more simple to try to merge the left node
                    first. So the strategy:
                    1. Pick the prev node to merge. (If leaf is most left, pick next node)
                    2. Update delete-key. (for prev, it's leaf[0]; for next, it's right key, and need
                    to update self)
                    3. Go up till reaching root. Do:
                    1. delete delete-key.
                    2. pick merging or stealing like above.
                    1. if merge, update delete-key, go up;
                    2. if steal, break to do update and has no need to go up.
                    4. Remember to deal with edge case: root.
                    */
                    + +

                    当删除后叶结点元素数过少,并且左右兄弟元素也都是最小值,那么需要与左右兄弟之一进行合并。优先合并左兄弟。合并都为大->小,也即target->左兄弟 或者 右兄弟->target。

                    +

                    需要递归删除父节点路径上的merge from元素。

                  6. -
                  7. 执行

                    -

                    两个操作数就绪后就执行

                    +
                6. -
                7. 写结果

                  +
                8. 可以看到,1/2/3三种情况都可以实现简单地直接返回。4稍显复杂,由于递归删除,所以需要对每一个父节点都再次进行上面几种策略的判断,直到遇到情况123返回为止。

                  +
                9. +
                +

                遇到的问题

                一个比较sb的小bug……

                +

                image-20231204163052528

                +

                Task5 Concurrency

                感想

                这位可更是重量级,足足花了我三天的时间……不过感觉第一次处理这么一个复杂的并发情景,花的时间还是值得的。

                +

                最后的结果虽然很一般(指排行榜倒数水平。。。),但至少还是过了。就先这样吧。

                +

                image-20231204170030245

                +

                思路

                我实现了crabbing lock+optimal lock。对于Insert和Remove,都是先在一开始获取header page和路径上父节点的读锁,然后在之后有可能向上更新时(比如说Insert的需要分裂、Remove的Update和Merge两种情况),丢弃所有读锁,然后获取header page和路径上父节点的写锁。

                +

                不过感觉我这个思路还是略有粗糙,因为相当一部分时间都得占用header page的写锁。但是我思考了一下细粒度方案,发现还是有点难实现。比如说,对于insert,细粒度化的方式也许就是一直持有header page的读锁,一直到需要分裂根节点时,才释放读锁获取写锁。但这样一来就会暴露一个危险的空窗期(而且感觉这个空窗期还不小),当你真的拿到写锁,这树的结构可能已经变得不知道什么样了。在这种情况下,你就需要再做一次回溯工作,也即获取从新root结点到旧root结点的路径,递归插入insert key和insert value,最后安全分裂根节点(因为此时已经安全持有了header page写锁)。感觉思路也是比较易懂,但是实现上还是太麻烦了,所以先暂且搁置吧。

                +

                遇到的问题

                这种感觉大多还是在面向测试用例见招拆招……所以其实感觉没什么好说的。

                +

                bpm遗留

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

                +

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

                +

                解决方法是要么写的时候持有bpm锁,但是这太太慢了。另一个就是干脆直接在unpin的时候不带bpm锁顺便写回了。也即把写回从evict后移到unpin中立即写回:

                +
                if (pages_[fid].GetPinCount() == 0 && pages_[fid].IsDirty()) {
                pages_[fid].is_dirty_ = false;
                disk_manager_->WritePage(pages_[fid].GetPageId(), pages_[fid].GetData());
                }
                + +

                火焰图性能分析

                +

                FlameGraph

                +

                参考博客

                +
                +

                看起来感觉大多性能损耗还是在bpm上,特别是LRU-K。也许是我的全局锁太暴力了。

                +

                out

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

                先放个通关记录~

                +

                image-20230330235300907

                +
                +

                特别鸣谢:

                +

                某不愿透露姓名的友人hhj

                +

                大佬的实验过程

                +

                大佬的性能优化

                +
                +
                +

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

                +

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

                +

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

                +

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

                +
                +

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

                +
                +

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

                +

                Your implementation will need to be thread-safe.

                +
                +

                总结

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

                +

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

                +

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

                +

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

                +

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

                +

                就酱。

                +

                Task1 LRU-K

                +

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

                +

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

                +

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

                +

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

                +

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

                +

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

                +
                +

                正确思路

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

                +

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

                  -
                1. 写入ROB,CDB传送ROB编号到保留站
                2. -
                3. 释放产生该结果的保留站
                4. +
                5. 如果已经被访问过k次: backward k-distance = current_timestamp_ - 倒数第k次访问的时间戳
                6. +
                7. 如果还没被访问过k次: backward k-distance = +inf
                -

                ROB字段:

                -
                  -
                • 指令类型

                  +

                  页面驱逐规则:

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

                    +

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

                  2. -
                  3. 目标地址

                    -

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

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

                  5. -
                  6. 数据值字段

                    -

                    前瞻结果

                    +
                  +

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

                  +
                  +

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

                  +

                  image-20230323205851168

                  +
                    +
                  1. 数据第一次被访问,加入到访问历史列表;

                  2. -
                  3. 就绪字段

                    -

                    结果是否就绪

                    +
                  4. 如果数据在访问历史列表里后没有达到K次访问,则按照一定规则(FIFO,LRU)淘汰;

                  5. -
                +
              4. 当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列删除,将数据移到缓存队列中,并缓存此数据,缓存队列重新按照时间排序;

              5. -
              6. 指令确认

                -

                分支结果出来后确认

                -
                  -
                1. 猜测对 写入寄存器/存储器,释放ROB
                2. -
                3. 猜测错 从另一条路径开始重新执行,清空ROB
                4. -
                +
              7. 缓存数据队列中被再次访问后,重新排序;

                +
              8. +
              9. 需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰“倒数第K次访问离现在最久”的数据。

              -

              例题

              img

              -

              img

              -

              img

              -

              img

              -

              img

              -

              img

              -

              题型6 超标量实现

              例题

              img

              -

              img

              -

              img

              -

              img

              -

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

              -

              img

              -

              img

              -

              img

              -

              img

              -

              img

              -

              题型7 循环展开

              具体步骤:

              -
                -
              1. 依题意展开
              2. -
              3. 去除多余的BNE、合并所有DADDUI
              4. -
              5. 寄存器换名消除名相关
              6. -
              7. 重排序消除数据相关
              8. -
              -

              1

              img

              -

              img

              -

              img

              -

              img

              -

              img

              -

              img

              -

              img

              -

              img

              -

              img

              -

              2

              img

              -

              img

              -

              img

              -

              题型8 VIEW技术

              例题

              img

              -

              img

              -

              img

              -

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

              -

              img

              -

              题型9 软流水

              例题

              img

              -

              img

              -

              img

              -

              img

              -

              指令级并行

              概念

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

                + +

                每个页面结构持有一个时间戳队列即可:

                +

                image-20231128152139485

                +

                感想

                刚coding完

                task1的内容就是实现对一堆frame_id的LRU-K算法管理,挺简单的(也可能是测试用例少我错误没排查出来2333)

                +

                我并没有用默认给的模板的unorder_map,也没有用默认给的模板思路(但原理以及最终效果是差不多的,就是没用它的方法),而是选择类似像上面这张图一样,分成两个队列实现,一个队列visit_record_存储那些访问次数<k的数据,另一个队列cache_data_存储那些访问次数>=k的顺序,每次优先淘汰visit_record_中的数据,两个队列都采用LRU的方式管理。与此同时,我觉得LRU管理时间戳只用记录最新访问的就行,所以将历史访问时间戳队列改成了只有一个变量。

                +

                终于通过online-test

                +

                参考:

                +

                FIFO和LRU这里面的实例非常直观地说明了两种算法的差异,可以跟着手推感受一下

                +

                pro1这个用的是我上面的那个想法,是错的。但是评论很值得参考:

                +

                image-20230329230045194

                +

                pro1这个评论的“偷测试用例”xswl,虽然这次没用,但以后说不定能用上:

                +

                image-20230329230139300

                +
                +
                正确思路

                ……简单个屁!!

                +

                算法上,上面错误的算法确实很简单;而正确的算法也确实很简单。那么难的是什么呢?我觉得难的还是搞清楚它要我们实现的究竟是上面东西。

                +

                结合指导书这段话:

                +
                +

                The LRU-K algorithm evicts a frame whose backward k-distance is maximum of all frames in the replacer. 每次驱逐 backward k-distance最大的

                +

                那么 backward k-distance是什么?

                +

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

                +

                A frame with fewer than k historical accesses is given +inf as its backward k-distance. 没有达到k次访问的, backward k-distance为+inf。也就是说,每次优先从历史访问队列清除元素。

                +

                【**When multiple frames have +inf backward k-distance, the replacer evicts the frame with the earliest overall timestamp (i.e., the frame whose least-recent recorded access is the overall least recent access, overall, out of all frames).**】当历史访问队列有多个元素,就驱逐英文描述那样的frame。

                +
                +

                我们可以发现,它这个对于两个队列的LRU,并非我们原来算法那样,对于每个frame,针对其最新的访问时间戳,也即history_.back(),进行LRU淘汰;而是,针对其倒数第k新的访问记录,也即history_.front() && history_.size()<=k,进行LRU淘汰。

                +

                其中,由于历史访问队列的记录少于k个,因而其事实上从k-distance算法退化为了FIFO算法。【感受一下这一点的优美:FIFO实际上是k-distance的特例】

                +

                我们上面的算法比较的是history_.back(),所以可以省略时间戳队列为一个变量,然后将两个队列使用FILO的形式组织起来。正确算法就不能这么简单了,要按front排序的话,实现开销可能更大,所以下面就采用了map形式来实现logn的查找。

                +
                关于LRU的翻译

                这里一个点我其实还是很疑惑的,完全想不通。

                +

                就是,对缓存队列实现k-distance算法没毛病,这段话已经写得很清楚了。

                +
                +

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

                +
                +

                但是,为什么历史访问队列要用FIFO呢?是我英语不好吗,这段话不是实现纯正LRU的意思吗:

                +
                +

                【**When multiple frames have +inf backward k-distance, the replacer evicts the frame with the earliest overall timestamp (i.e., the frame whose least-recent recorded access is the overall least recent access, overall, out of all frames).**】当历史访问队列有多个元素,就驱逐英文描述那样的frame。

                +
                +

                我翻译一下:

                +

                当多个frame有+inf这个 backward k-distance的时候,replacer需要驱逐拥有全部(overall)frame中最早的timestamp的frame。(也就是说,frame,它的最近访问记录是所有frame里面最早的)

                +

                这样确实看起来就是要用LRU。

                +

                但其实,是我英语不好。咨询了场外热心人士hhj之后,我才修订出了如下版本:

                +

                当多个frame有+inf这个 backward k-distance的时候,replacer需要驱逐拥有全部(overall)frame中最早的timestamp的frame。(也就是说选择一个frame,这个frame的最不近的访问记录,是所有frame中最近最少访问的)【也即这个frame的history的front是所有frame中最早的,也即使用FIFO算法】

                +

                可见,正确解法确实是没问题的,就是理解上很困难。要是可以配个实例就好了QAQ

                +

                所以说,所谓LRU(Least Recently Used)的直译还是最不近使用,也即最近最少使用。里面这个least不是用来修饰recent表示recent程度深的,相反它表示的是recent的程度浅。英语不好的惨痛教训啊。

                +

                image-20230330160207757

                +

                最后一下子交了这么多次才过。绷不住了。

                +

                Task2 BufferPoolManager

                +

                The BufferPoolManager is responsible for fetching database pages from the DiskManager and storing them in memory.从DiskManager中取出页,然后存入内存。

                +

                也就是说,我们的Buffer Pool是磁盘到内存的映射,我们在Task1实现了内存部分的管理数据结构?

                +

                The BufferPoolManager can also write dirty pages out to disk when it is either explicitly instructed to do so or when it needs to evict a page to make space for a new page.也要负责dirty页的写回

                +

                You will also not need to implement the code that actually reads and writes data to disk (this is called the DiskManager in our implementation). We will provide that functionality. DiskManager已给出

                +

                All in-memory pages in the system are represented by Page objects. Each Page object contains a block of memory that the DiskManager will use as a location to copy the contents of a physical page that it reads from disk. Page 是可复用的内存页容器

                +

                The Page object’s identifer (page_id) keeps track of what physical page it contains; if a Page object does not contain a physical page, then its page_id must be set to INVALID_PAGE_ID.

                +

                也就是说,page_id表示的是实际的物理页号;frame_id表示的是你的Page容器的序号,同时也是LRU的对象。你需要一个类似<fid, pid>这样的map来记录这二者的映射。具体是通过:

                +
                /** Array of buffer pool pages. */
                Page *pages_; // <fid, pid>
                /** Page table for keeping track of buffer pool pages. */
                std::unordered_map<page_id_t, frame_id_t> page_table_; // <pid, fid>
                + +

                Each Page object also maintains a counter for the number of threads that have “pinned” that page. Your BufferPoolManager is not allowed to free a Page that is pinned. 有引用计数机制

                +

                Each Page object also keeps track of whether it is dirty or not. It is your job to record whether a page was modified before it is unpinned. Your BufferPoolManager must write the contents of a dirty Page back to disk before that object can be reused.需要track dirty,并且这是你要干的;要写回,这也是你要干的

                +

                Your BufferPoolManager implementation will use the LRUKReplacer class that you created in the previous steps of this assignment. The LRUKReplacer will keep track of when Page objects are accessed so that it can decide which one to evict when it must free a frame to make room for copying a new physical page from disk. When mapping page_id to frame_id in the BufferPoolManager, again be warned that STL containers are not thread-safe.

                +
                +

                感想

                刚coding完

                task2说得比较复杂,实现的函数较多,实际coding细节也比较繁琐,但debug倒是很轻松。

                +

                主要内容就是实现BufferPoolManager,在task1实现的LRU-K算法的基础上,写具体的内存换入换出的接口逻辑。

                +

                再次回顾我们整个project1的目的:实现一个从磁盘到内存的buffer。task1只是实现了一个内存页换入换出的LRU-K算法部分,task2则基于算法部分,实现了具体与上层交互的像样的逻辑。

                +

                我认为这其中一个亮点就是,它非常完美地将LRU-K算法和具体的上层逻辑进行了解耦。LRU-K只需关注如何将这一堆freme_id组织起来组织好,而无需关心具体内存页存放在哪,以及对应frame淘汰之后内存页又何去何从,因为这些逻辑都会由上层实现;而上层逻辑也无需关心具体的淘汰页算法【LRU-K/LRU/LFU,只需替换replacer_就可以替换换入换出策略】,而只需打好evictable标记,并且在调用evict方法之后做好后处理(如内存释放等等等)即可。

                +

                这其中有一个小细节也值得借鉴,即从page_id_frame_id_的转化。frame_id_有界,比较方便LRU-K算法实现,并且进行了LRU-K算法的容量控制,同时由于算法和上层逻辑的容量相同,故而也是pages_的索引号;而page_id_不能有界,因为实际上访问到的物理页不可能只共享pool_size_个序列号。故而在这样解耦实现的基础上,二者缺一不可。

                +

                还有frame_id_的复用,它是采用了类似我们日常生活中取号那样,要用号时从队列头取,不用号时塞回队列尾就行,这种方式我觉得还挺有意思。

                +

                其他部分虽然步骤繁杂,但理解难度不高,而且它提示得也很保姆了,所以不多bb。

                +

                通过online-test

                确实算简单了,我主要倒在没有认真看它的需求,这应该是语文问题(绷

                +

                一个是FetchPage这里:

                +

                image-20230330162812680

                +

                如果所求物理页存在于buffer pool,直接返回+record access即可,不用再写回+读入。因为它的提示这边:

                +

                image-20230330162941774

                +

                这个是句号。也就是说后面那些写回啊read啊,是没找到时才做的,不是并列关系。

                +

                这也很合理,毕竟你找到所需页就说明不用从磁盘读入,也即找到所需页=直接返回即可。

                +

                另一个是UnpinPage这里:

                +

                image-20230330163031739

                +

                不应该写is_dirty_ = is_dirty,因为它的提示这边:

                +

                image-20230330163058921

                +

                可见参数is_dirty为true是需要设置为dirty,为false的话没有别的意义,保持原来值就行。

                +

                还有一个就是,在Page类中声明了friend:

                +

                image-20230330163337929

                +

                故而BufferPoolManager可以直接访问Page的私有成员变量,而无需手动为Page添加Getter/Setter方法。

                +

                Task3 Page Guard

                这是要写我们在上面用的那个PageGuard?这让我想起了Lab0的ValueGuard

                +
                template <class T>
                class ValueGuard {
                public:
                ValueGuard(Trie root, const T &value) : root_(std::move(root)), value_(value) {}
                auto operator*() const -> const T & { return value_; }

                private:
                Trie root_;
                const T &value_;
                };
                + +

                不过其实这两个是不一样的。本次要实现的Page Guard的语义更类似lock_guard

                +
                +

                我们需要手动调用UnpinPage,但这中就跟new/delete、malloc/free一样都要靠人脑来记住,不大安全。

                +

                You will implement BasicPageGuard which store the pointers to BufferPoolManager and Page objects. A page guard ensures that UnpinPage is called on the corresponding Page object as soon as it goes out of scope. 【也许这需要在析构函数中实现?】Note that it should still expose a method for a programmer to manually unpin the page.仍然需要提供UnPin方法。

                +

                As BasicPageGuard hides the underlying Page pointer, it can also provide read-only/write data APIs that provide compile-time checks to ensure that the is_dirty flag is set correctly for each use case.这个思想很值得学习。

                +

                In the future projects, multiple threads will be reading and writing from the same pages, thus reader-writer latches are required to ensure the correctness of the data. Note that in the Page class, there are relevant latching methods for this purpose. Similar to unpinning of a page, a programmer can forget to unlatch a page after use. To mitigate the problem, you will implement ReadPageGuard and WritePageGuard which automatically unlatch the pages as soon as they go out of scope.

                +
                +

                感想

                怎么说,其实只用仔细看相关文档和它的要求就不难,但你懂的我的尿性就是不细看文档,所以这里我也用gdb调了蛮久才过的。正确思路没什么好说的,直接记录下我觉得比较有意义的错误吧。

                +

                错误集锦

                析构函数的调用

                image-20230330233252372

                +

                在这个用例中,退出“}”会调用两次析构函数。

                +
                奇怪的死锁
                debug过程

                我在coding的过程中,遇到了一个很神奇的死锁现象。

                +

                在这里page->WLatch();这句会死锁,而且还是在第一次调用FetchWritePage()时死锁的:

                +
                WritePageGuard(BufferPoolManager *bpm, Page *page) : guard_(bpm, page) {
                page->WLatch();
                }
                + +

                但是添加了一句page->WUnlatch();

                +
                WritePageGuard(BufferPoolManager *bpm, Page *page) : guard_(bpm, page) {
                page->WUnlatch();
                page->WLatch();
                }
                + +

                它就不会死锁了。

                +

                这很奇怪,到底是发生了什么?我用GDB调了半天,在RWLatch.WLock()处打了断点,也没发现在这之前有调用过lock()。于是我就去看了下std::shared_mutex的官方文档(当然,这中间想了很久也不知道怎么办):

                +

                image-20230331222601644

                +

                我就怀疑是不是我哪里写错了,所以就干了这种undefined的事,然后就导致死锁了。于是我写了个测试程序:

                +

                image-20230330195418370

                +

                发现,当在调用WLock(也即std::shared_mutex::lock())之前,如果多调了一次XUnlock(也即std::shared_mutex::unlock()或者std::shared_mutex::unlock_shared()),就会卡住。

                +

                这说明确实发生了不匹配问题。于是我就在Page中添加了两个成员变量用来记录上锁和解锁的次数,并且在gurad test中打印了出来,结果发现:

                +

                image-20230330233018049

                +

                确实发生了不匹配问题,是在这里:

                +

                image-20230330233252372

                +

                之后用gdb调下就发现错误了,不赘述了。

                +
                另外的想法

                在出现死锁问题时,我是想着,会不会是测试程序中,对同一页获取了一次ReadGuardPage对象之后,再对同一页获取Read/WriteGuardPage导致的呢?于是我就开始思考如何防范这个流程,最后写下了这样的代码:

                +
                auto BufferPoolManager::FetchPageRead(page_id_t page_id) -> ReadPageGuard {
                Page *page = FetchPage(page_id);
                bool should_release = true;
                if (!page->rwlatch_.try_lock_shared()) {
                // 说明此时已有read/write锁
                should_release = false;
                }
                return {this, pagei, should_release};
                }

                auto BufferPoolManager::FetchPageWrite(page_id_t page_id) -> WritePageGuard {
                Page *page = FetchPage(page_id);
                bool should_release = true;
                if (!page->rwlatch_.try_lock()) {
                // 获取write锁失败,可能原因:该进程持有write锁、别的进程有read锁、该进程持有read锁
                if (page->rwlatch_.try_lock_shared()) {
                // 成功read,说明是别的进程有read锁
                page->rwlatch_.unlock_shared();
                // 等待
                page->rwlatch_.lock();
                } else {
                // 说明当前进程有read/write锁
                should_release = false;
                }
                }
                return {this, pagei, should_release};
                }
                + +

                但很遗憾的是,我发现是无法区分当前进程持有write还是read锁的。也许有别的办法但我没想起来。

                +

                总之,我认为这段代码还是很有参考价值的,姑且放着先。

                +

                Task4 性能调优

                +

                参考:

                +

                CMU 15-445 2023 P1 优化攻略 [rank#3] 写得非常细致,思路很清晰

                +

                CMU 15-445 Project 1 (Spring 2023) 优化记录

                +
                +
                +

                我的实现有一些并发小问题,详见lab2的并发部分~

                +
                +

                lru-k的算法优化是自己想的,并行IO的优化思路全部来自 CMU 15-445 Project 1 (Spring 2023) 优化记录,我只是把这位大佬的思路自己实现了一遍。感觉还是太菜了,面对这种实际场景毫无还手之力一点思路没有QAQ但正是如此,这个细粒度化锁的小task才值得学习。

                +

                放上优化前后性能对比:

                +

                image-20230331000020775

                +

                image-20230404140838247

                +

                Better replacer algorithm

                +

                In the leaderboard test, we will have multiple threads accessing the pages on the disk. There are two types of threads running in the benchmark:在具体的benchtest中,可以分为两类线程。

                  -
                1. 基于硬件的动态开发
                2. -
                3. 基于软件的静态开发
                4. -
                -
              2. -
              3. 流水线CPI

                -

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

                -
              4. -
              5. 理想CPI是衡量流水线最高性能

                -
              6. -
              7. IPC:每个时钟周期完成的指令数

                -

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

                -
              8. -
              9. 基本程序块:一串没有分支和跳转转入点的指令块

                -
              10. +
              11. Scan threads. Each scan thread will update all pages on the disk sequentially. There will be 8 scan threads.
              12. +
              13. Get threads. Each get thread will randomly select a page for access using the zipfian distribution. There will be 8 get threads.
              -

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

              -

              相关与并行

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

              -

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

              -

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

              -

              约定先执行i再执行j

              -
                -
              1. 数据相关

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

                  -
                2. -
                3. 具有传递性

                  -
                4. -
                5. 反映数据流动关系,即如何从生产者流动到消费者

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

                  -
                8. -
                9. 解决方法

                  +

                  Given that get workload is skewed(有偏向性的)(i.e., some pages are more frequently accessed than others), you can design your LRU-k replacer to take page access type into consideration, so as to reduce page miss.

                  + +

                  解决方法

                  我们可以回想起当初选择LRU-K而不选择LRU算法的原因:缓存污染。

                  +
                  +

                  LRU 一种缓存淘汰算法

                  +

                  缓存污染:

                  +

                  LRU因为只需要一次访问就能成为最新鲜的数据,当出现很多偶发数据时,这些偶发的数据也会被当作最新鲜的,从而成为缓存。但其实这些偶发数据以后并不会是被经常访问的。

                  +
                  +

                  而在这里也是同理。我们的benchtest中,scan线程是顺序地访问磁盘上所有页,而get线程是遵从zip分布地访问,显然get线程的access记录比scan线程的有价值的多,并且scan线程的数据是很容易污染get线程的。

                  +

                  所以,我的解决方法是,如果某个页被第一次访问,且该访问方式为SCAN,则RecordAccess进入历史访问队列;如果某个页不是被第一次访问,且访问方式为SCAN,则不做任何处理。不用修改UnpinPage的处理方式。

                  +

                  Parallel I/O operations

                  +

                  Instead of holding a global lock when accessing the disk manager【不要在访问disk_manager_的时候使用bpm的全局锁latch_】, you can issue multiple requests to the disk manager at the same time. This optimization will be very useful in modern storage devices, where concurrent access to the disk can make better use of the disk bandwidth.

                  +
                  +

                  解决方法

                  详细的解决方法大佬这边已经说得很清楚了,接下来我就对其总体的做法进行一点总结,加上一些个人理解。

                  +

                  我刚看到这个需求的时候是这么做的:

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

                  也即在原来代码的基础上做简单的改动,每次执行到涉及磁盘读写的地方,就暂时地开一下锁。但其实这样是不行的,当多个线程访问bpm,线程A在这里开锁执行Write,线程B正好得到锁,然后对pages_[fid]执行比如说ResetMemory操作,这样就寄了。

                  +

                  所以,在磁盘读写的时候,我们仍然需要使用锁保护,只不过我们需要选择粒度更细的锁。这时我们就可以想到在page_guard里常用的page自带的锁。在这里用page锁,既能够锁保护,又符合语义,看起来非常完美:

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

                  但由于我们在returnpage_guard的时候会获取锁,因而在这样的情况下,会发生死锁:

                  +
                  auto reader_guard_1 = bpm->FetchPageRead(page_id_temp);
                  auto reader_guard_2 = bpm->FetchPageRead(page_id_temp);
                  + +
                  +

                  在这里我们首先获取reader_guard_1 ,持有了该 page 的读锁,并允许其他线程读;但在获取reader_guard_2时,FetchPage会在释放 bpm 写锁前,请求该 page 的写锁;但由于reader_guard_1已经申请了该 page 的读锁,就会造成死锁,与预期结果不符。

                  +
                  +

                  因而,我们就可以选择在bpm内部,单独为pages_数组的每一页都维护一个锁,在每个对page页属性进行读写的地方进行锁定:

                  +
                  std::shared_mutex latch_;
                  std::vector<std::mutex> pages_latch_;
                  + +

                  然后对代码进行重排序,尽量分离bpm内部成员和page内部成员属性的修改:(以FetchPage为例)

                  +
                  auto BufferPoolManager::FetchPage(page_id_t page_id, [[maybe_unused]] AccessType access_type) -> Page * {
                  ...
                  if (free_list_.empty()) {
                  frame_id_t fid;
                  if (!replacer_->Evict(&fid)) { ... }
                  // 这些地方不涉及对page的读写,只涉及对bpm内部成员的读写
                  page_table_.erase(page_table_.find(pages_[fid].GetPageId()));
                  page_table_.insert(std::make_pair(page_id, fid));

                  replacer_->RecordAccess(fid, access_type);
                  replacer_->SetEvictable(fid, false);

                  // 两个锁的交接点
                  pages_latch_[fid].lock();
                  latch_.unlock();

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

                  Page *res = &(pages_[fid]);
                  res->page_id_ = page_id;
                  res->ResetMemory();
                  disk_manager_->ReadPage(page_id, res->GetData());
                  res->is_dirty_ = false;

                  res->pin_count_ = 1;

                  pages_latch_[fid].unlock();
                  return res;
                  }
                  // 这些地方不涉及对page的读写,只涉及对bpm内部成员的读写
                  frame_id_t fid = free_list_.front();
                  free_list_.pop_front();
                  page_table_.insert(std::make_pair(page_id, fid));

                  replacer_->RecordAccess(fid, access_type);
                  replacer_->SetEvictable(fid, false);

                  // 两个锁的交接点
                  pages_latch_[fid].lock();
                  latch_.unlock();

                  Page *res = &(pages_[fid]);
                  res->page_id_ = page_id;
                  res->ResetMemory();
                  disk_manager_->ReadPage(page_id, res->GetData());
                  res->is_dirty_ = false;

                  res->pin_count_ = 1;
                  pages_latch_[fid].unlock();

                  return res;
                  }
                  + +

                  其他地方也是一样。就不多赘述了。

                  +
                  一个小地方

                  当外界需要对页进行读写时,需要使用page自带的锁;而当bpm内部需要对页进行读写时,则使用的是bpm内部自带的页锁。

                  +

                  这句话说完,相信危险性已经显而易见了:我们使用了两把不同的锁维护了同一个变量!而且可能会有两个线程分别持有这两个锁,对这个变量并发更新!

                  +

                  但其实,在当前这个场景,这么做是没问题的。

                  +

                  外界实质上只能对page的data字段进行读写。因而,有上述危险的,实质上就只有bpm中会对data字段进行改变的地方,也即bpm::NewPage()bpm::FetchPage()bpm::DeletePage()这三个地方。

                  +

                  而在前两个地方,我们会使用到的page都是闲置/已经被释放的页,因而外界不可能,也即不可能有别的线程,会持有page的锁并且对其修改;同样的,在第三个地方,我们会使用的page也是pincount==0的页,仅有当前线程在对其进行读写。

                  +

                  因而,综上所述,这样做是并发安全的。

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

                  实验官网

                  +

                  代码

                  + +

                  Project0 C++ Primer

                  Project1 Buffer Pool

                  Project2 B+Tree Index

                  Project3 Query Execution

                  +

                  In this project, you will implement the components that allow BusTub to execute queries. You will create the operator executors that execute SQL queries and implement optimizer rules to transform query plans.

                  +

                  实现SQL查询的执行,并且实现语句优化。

                  +
                  +

                  Background

                  Bustub Framewor

                  image-20231227153858926

                  +

                  AST

                  介绍完了bustub的框架之后,它对通过语法树进行查询优化进行了详细的样例介绍。

                  +

                  首先温习一下什么是语法树(abstract syntax tree, AST ):

                  +

                  SQL语句

                  +
                  Select `title`
                  From Books, Borrowers, Loans
                  Where Books.LC_NO = Loans.LC_NO and Borrowers.CARD_NO = Loans.CARD_NO
                  + +

                  其语法树表示+优化结果如下图所示:

                  +

                  image-20231227155236633

                  +

                  算法如下,其关键思路就是选择投影尽早做,能移多下去就移多下去

                  +

                  image-20231227155806019

                  +

                  而这里15445介绍的也是这样的语法树优化算法。

                  +

                  首先记录一下它这几个专有名词对应的操作:

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

                    -

                    调度

                    -
                  2. -
                  3. 变换代码消除相关关系

                    -
                  4. -
                  -
                10. -
                11. 检测方法

                  -

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

                  -
                12. +
                13. Projection:投影
                14. +
                15. Filter:选择
                16. +
                17. MockScan:对一个表进行的扫描操作
                18. +
                19. Aggregation:聚合函数
                20. +
                21. NestedLoopJoin:嵌套循环连接
                -
              2. -
              3. 名相关

                -

                img

                -
                  -
                1. 分类

                  + +

                  再结合它给的几个语法树的例子:

                  +
                  SELECT * FROM __mock_table_1;

                  === PLANNER ===
                  Projection { exprs=[#0.0, #0.1] } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
                  MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
                  === OPTIMIZER ===
                  MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
                  + +
                  SELECT colA, MAX(colB) FROM
                  (SELECT * FROM __mock_table_1, __mock_table_3 WHERE colA = colE) GROUP BY colA;

                  === OPTIMIZER ===
                  Agg { types=[max], aggregates=[#0.1], group_by=[#0.0] }
                  NestedLoopJoin { type=Inner, predicate=(#0.0=#1.0) }
                  MockScan { table=__mock_table_1 }
                  MockScan { table=__mock_table_3 }
                  + +

                  image-20231227160450894

                  +
                  SELECT * FROM __mock_table_1 WHERE colA > 1;

                  === OPTIMIZER ===
                  Filter { predicate=(#0.0>1) } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
                  MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
                  + +
                  values (1, 2, 'a'), (3, 4, 'b');

                  === PLANNER ===
                  Projection { exprs=[#0.0, #0.1, #0.2] } | (__values#0.0:INTEGER, __values#0.1:INTEGER, __values#0.2:VARCHAR)
                  Values { rows=2 } | (__values#0.0:INTEGER, __values#0.1:INTEGER, __values#0.2:VARCHAR)
                  === OPTIMIZER ===
                  Values { rows=2 } | (__values#0.0:INTEGER, __values#0.1:INTEGER, __values#0.2:VARCHAR)
                  + +

                  可以看到,它大概是用缩进来表示了AST的父子关系。

                  +

                  我们课上学习的语法树中每个table标志对应着一个MockScan;笛卡尔积+选择操作可以表示为一个NestedLoopJoin。

                  +

                  对于这些输出的意义,指导书也给了详细的解释:

                  +

                  ColumnValueExpression

                  +

                  也即类似exprs=[#0.0, #0.1]#0意为第一个子节点(不是第一个表的意思。。)

                  +

                  Project

                  +

                  In this project, you will add new operator executors and query optimizations to BusTub. BusTub uses the iterator (i.e., Volcano) query processing model, in which every executor implements a Next function to get the next tuple result. When the DBMS invokes an executor’s Next function, the executor returns either (1) a single tuple or (2) an indicator that there are no more tuples. With this approach, each executor implements a loop that continues calling Next on its children to retrieve tuples and process them one by one.

                  +
                  +]]> + + labs + + + + 编译原理 + /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. 反相关:先读后写,也即i读的名与j写的名相同
                  2. -
                  3. 输出相关:i和j写的名相同
                  4. +
                  5. 收集标识符的属性信息,并将其存入符号表
                  -
                2. -
                3. 解决方法:换名技术,可以编译器静态实现 or 硬件动态实现

                  -

                  img

                  -
                4. -
                5. 相关问题

                  -

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

                  -
                    -
                  1. WAR(反相关)
                  2. -
                  3. WAW(输出相关)
                  4. +

                    picture

                    +

                    种属就是比如是函数还是数组之类的。

                    +

                    picture

                    +
                      +
                    1. 语义检查
                    +

                    picture

                    +
                      +
                    1. 静态绑定

                      +

                      包括绑定代码相对地址(子程序)、数据相对地址(变量)

                    - -
                  5. 数据冲突

                    -

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

                    +

                    中间代码生成

                    picture

                    +

                    picture

                    +

                    波兰也就是前序遍历二叉树(中左右),逆波兰也就是后序遍历二叉树(左右中)

                    +

                    picture

                    +

                    代码优化

                    picture

                      -
                    1. RAW(数据相关)

                      -

                      也即i写j读

                      -
                    2. -
                    3. WAW(输出相关)

                      -

                      也即i写j写

                      -

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

                      -

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

                      +
                    4. 无关机器

                      +

                      picture

                    5. -
                    6. WAR(反相关)

                      -

                      也即i读j写

                      -

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

                      +
                    7. 有关机器

                      +

                      picture

                    -
                  6. -
                  7. 控制相关

                    -

                    由分支指令引起

                    -
                  8. +

                    目标代码生成

                    picture

                    +

                    表格管理

                    这也挺好理解,相当于管理符号表吧。

                    +

                    picture

                    +

                    错误处理

                    picture

                    +

                    编译程序的组织

                    了解了编译程序的基本结构,那么我们就可以想想该怎么实现这个编译器了。

                    +

                    最直观的想法是,我们有几个步骤就对代码进行多少次扫描:

                    +
                      +
                    1. 首先扫一次,进行词法分析,将所有标识符写入到符号表中,同时进行语法分析,看看有没有错,如果出错了就转到错误处理,没有的话就进行语义分析;(三合一)
                    2. +
                    3. 然后再针对得出来的语义分析树进行中间代码生成;
                    4. +
                    5. 再对得出来的中间代码进行代码优化,最后对优化出来的代码进行翻译处理。(二合一)
                    -

                    调度

                    img

                    -

                    img

                    -

                    动态调度

                    基本思想

                    img

                    -

                    img

                    -

                    img

                    -

                    img

                    -

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

                    -

                    img

                    -

                    记分牌动态调度算法

                      -
                    1. 基本思想

                      -

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

                      -
                    2. -
                    3. 基本结构

                      -

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

                      +

                      picture

                      +

                      picture

                      +

                      picture

                      +

                      实现编译器

                      picture

                      +

                      T形图

                      picture

                      +

                      自展

                      picture

                      +

                      也就是说:

                        -
                      1. 指令状态表

                        -

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

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

                        -

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

                        -
                          -
                        • Busy:yes/no
                        • -
                        • Op:操作编码
                        • -
                        • Fi:目的寄存器编号
                        • -
                        • Fj,Fk:源寄存器编号
                        • -
                        • Qj,Qk:Fj和Fk的功能部件
                        • -
                        • Rj,Rk:Fj和Fk是否就绪且还没被取走
                        • -
                        +
                      4. P0是汇编语言,可以用来编译C语言子集;(P0:汇编语言,C子集→汇编)
                      5. +
                      6. P1是机器语言,可以用来把汇编语言翻译为机器语言;(P1:机器语言,汇编→机器)
                      7. +
                      8. 所以我们就得到了P2,也即一个可以用来编译C语言子集的机器语言程序;(P2:机器语言,C子集→汇编)
                      9. +
                      10. 然后我们就可以用C语言子集来写C语言编译程序P3,再用P2翻译P3,就可以得到工具P4。(P4:汇编语言,C→汇编)
                      11. +
                      +

                      image-20230912153726618

                      +

                      帅的。

                      +

                      移植

                      picture

                      +

                      picture

                      +

                      本机编译器的利用

                      picture

                      +

                      编译程序的自动生成

                      这大概是描述了我们到时候会怎么实现这两个阶段代码。

                      +

                      不过确实,词法分析可以看作是正则匹配,语法分析可以看作是产生式。

                      +

                      picture

                      +

                      picture

                      +

                      第二章 文法等概念

                      image-20231111160656018

                      +

                      基本概念

                        +
                      1. 字母表

                        +

                        picture

                        +

                        picture

                        +

                        picture

                        +

                        picture

                      2. -
                      3. 结果寄存器状态表

                        -

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

                        -

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

                        +
                      4. +

                        克林闭包中的每一个元素都称为是字母表Σ上的一个串

                        +

                        picture

                        +

                        picture

                        +

                        picture

                      -
                    4. -
                    5. 执行流程

                      -

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

                      +

                      文法

                      picture

                      +

                      如果文法用于描述单词,基本符号就是字母;用于描述句子,基本符号就是单词

                        -
                      1. 流出

                        -

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

                        +
                      2. 文法的形式化定义

                        +

                        picture

                        +

                        picture

                        +

                        由于可以从它们推出其他语法成分,故而称之为非终结符

                        +

                        picture

                        +

                        picture

                        +

                        还真是最大的语法成分

                      3. -
                      4. 读操作数

                        -

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

                        +
                      5. 产生式

                        +

                        picture

                      6. -
                      7. 写结果

                        -

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

                        +
                      8. 符号约定

                        +

                        picture

                        +

                        picture

                        +

                        picture

                        +

                        文法符号串应该就是指既包含终结符也包含非终结符的,也可能是空串的串。

                        +

                        注意终结符号串也包括空串。

                      +

                      语言

                      picture

                      +

                      这部分就是要讲怎么看一个串是否满足文法规则,那么我们就需要先从什么样的串是满足文法规则的串开始说起,也即引入“语言”的概念。

                      +
                        +
                      1. 推导与归约

                        +

                        picture

                        +

                        然后也分为最左推导和最右推导,对应最右归约和最左归约。

                        +

                        picture

                        +

                        故而,如果从开始符号可以推导(派生)出该句子,或者从该句子可以归约到开始符号,那么该句子就是该语言的句子。

                      2. -
                      3. 性能分析

                        -

                        img

                        +
                      4. 句子与句型

                        +

                        picture

                        +

                        句型就是可以有非终结符,句子就是只能有终结符

                      5. -
                      -

                      Tomasulo算法

                        -
                      1. 核心思想

                        -

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

                        -

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

                        -

                        img

                        +
                      2. 语言

                        +

                        picture

                        +

                        文法解决了无穷语言的有穷表示问题。

                        +

                        picture

                        +

                        picture

                        +

                        emm,就是好像没有∩运算

                        +

                        picture

                        +

                        有正则那味了

                      3. -
                      4. 基本结构

                        +
                      +

                      乔姆斯基文法体系

                      picture

                      +

                      picture

                        -
                      1. 保留站

                        -

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

                        -

                        img

                        -

                        img

                        -

                        故而,需要有以下字段:

                        -
                          -
                        • Op:操作

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

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

                          -

                          load的Vk保存偏移量

                          +
                        • 0型

                          +

                          picture

                        • -
                        • Busy

                          +
                        • 1型

                          +

                          picture

                          +

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

                          +

                          CSG不包含空产生式。

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

                          +
                        • 2型

                          +

                          picture

                          +

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

                        • -
                        • Qi:寄存器状态表

                          -

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

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

                        -

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

                        +
                      4. 3型

                        +

                        picture

                        +

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

                        +

                        picture

                        +

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

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

                        -
                          -
                        1. load缓冲器
                            -
                          1. 存放用于计算有效地址的分量
                          2. -
                          3. 记录正在进行的load访存
                          4. -
                          5. 保存buffer等待CDB传输
                          -
                        2. -
                        3. store缓冲器
                            -
                          1. 存放用于计算有效地址的分量
                          2. -
                          3. 记录正在进行的store访存,如目标地址以及是否已有数据
                          4. -
                          5. 保存buffer等待CDB传输
                          6. +

                            CFG

                            正则文法用于判定大多数标识,但是无法判断句子构造

                            +
                              +
                            1. 分析树
                            - +

                            picture

                            +

                            picture

                            +

                            也就是说,每个句型都有自己对应的分析树。那么接下来就介绍什么是句型的短语

                            +

                            picture

                            +

                            意思就是直接短语是高度为2的子树的边缘,直接短语一定是某个产生式的右部,但是产生式右部不一定是给定句型的直接短语(因为有可能给定句型的推导用不到那个产生式)

                            +
                              +
                            1. 二义性文法
                            +

                            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. 浮点寄存器FP

                              -

                              img

                              -
                            4. -
                            5. 指令队列

                              -

                              FIFO

                              +
                            6. 否则,看输入符号是否与Follow集元素匹配

                              +
                                +
                              1. +

                                若B无空产生式,报错;否则,使用B的空产生式(相当于消了一个符号但不变输入带指针)

                              2. -
                              3. 运算部件

                                -

                                浮点加法器、浮点乘法器

                                +
                              4. +

                                报错

                            7. -
                            8. 寄存器换名实现

                              -

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

                              -
                            9. -
                            10. 特点

                              +
                            +

                            picture

                            +

                            这个感觉跟first集有点像,相当于是右部只能以终结符开始的形式,所以下面的LL文法会增强定义。

                            +

                            当该非终结符对应的所有SELECT集不相交,就可以进行确定的自顶向下语法分析。这个思想也将贯穿下面的LL文法

                            +

                            picture

                            +

                            LL(1)文法

                            picture

                            +

                            picture

                            +

                            最后,如果同一非终结符的各个产生式的可选集互不相交,就可以进行确定的自顶向下分析:

                            +

                            picture

                            +

                            picture

                            +

                            总结

                            这几个推理下来,真是让人感觉酣畅淋漓!

                            +

                            确定的自顶向下分析的核心就是,给定一个当前所处的非终结符和一个输入字符[E, a],我们可以唯一确定一个产生式P用于构建语法分析树。

                            +

                            picture

                            +

                            也即,同一个非终结符的所有产生式的SELECT集必须是不交的【才能确保选择产生式的唯一性】。因而,问题就转化为了如何让SELECT集不交

                            +

                            我们需要对空产生式和正常产生式的SELECT集计算做一个分类讨论。

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

                              -

                              通过保留站和CDB实现

                              -

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

                              +
                            2. 空产生式

                              +

                              由于可以推导出空,相当于把该符号啥了去读下一个符号,因此我们的问题就转化为输入字符a是否能够跟该符号后面紧跟着的字符相匹配。而紧跟着的字符集我们将其成为FOLLOW集,如果a在follow集中,那么就可以接受,否则不行。

                              +

                              对于LL(1)文法,相当于是进一步处理了简介推出空的串:

                              +

                              ​ 由于α串->*空,则α串必定仅由非终结符构成。那么它能推导出的所有可能即为SELECT集。故而为First(α)∪Follow(α)

                            3. -
                            4. 消除了WAW和WAR

                              +
                            5. 非空产生式

                              +

                              很简单,就是其First集。

                            - -
                          7. 执行步骤

                            -

                            3段流水

                            +

                            故而,只需要让这些计算出来的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. 流出

                              -

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

                              -
                            2. -
                            3. 执行

                              -

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

                              -

                              img

                              +
                            4. 移进状态

                              +

                              ·后为终结符

                            5. -
                            6. 写结果

                              -

                              计算完毕后由CDB传送

                              +
                            7. 待约状态

                              +

                              ·后为非终结符

                            8. -
                            +
                          8. 归约状态

                            +

                            ·后为空

                          -

                          基于硬件的前瞻执行

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          多指令流出

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          超标量实现

                          img

                          +

                          picture

                          +

                          picture

                          +

                          以前感觉一直很难理解GOTO表的作用,现在感觉稍微明白了点了,你想想,归约之后的那个结果是不是有可能是另一个产生式的右部成分之一,也即一个新的句柄?并且这个也是由你栈顶刚归约好的那个左部和下面的输入符号决定的。那么你自然而然需要切换一下当前状态,以便之后遇到那个产生式的时候能发现到了。

                          +

                          那么,剩下的问题就是如何构造LR分析表了:

                          +

                          picture

                          +

                          算符分析

                          picture

                          +

                          也就是它会整一个终结符之间的优先级关系。。。

                          +

                          picture

                          +

                          picture

                          +

                          也就是说:

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

                            -

                            整数型:load、store、分支

                            -

                            浮点型:可能各种运算吧

                            +
                          2. a=b

                            +

                            相邻

                          3. -
                          4. 假设所有浮点指令都是加法,执行时间3个时钟周期,且图中整数总在浮点前

                            +
                          5. a<b

                            +

                            也即在A->aB时,b在FIRSTOP(B)中(理解一下,这个First指在前面。。。)

                            +
                          6. +
                          7. a>b

                            +

                            也即在A->Bb时,a在LASTOP(B)中(理解一下,这个LAST指在后面。。。)

                          -

                          img

                          -

                          img

                          -

                          img

                          -

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

                          -

                          img

                          -

                          基于静态调度

                          img

                          -

                          基于动态调度

                          img

                          -

                          img

                          -

                          img

                          -

                          VLIW技术

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          基本指令调度和循环展开

                          img

                          -

                          img

                          -

                          指令调度

                          img

                          -

                          img

                          -

                          img

                          -

                          循环展开

                          img

                          -

                          img

                          -

                          软流水

                          img

                          -

                          img

                          -

                          03 AI处理器

                          并行体系结构

                          分类

                          SISD

                          img

                          -

                          SIMD

                          img

                          -

                          img

                          -

                          MIMD

                          img

                          -

                          向量体系结构

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

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

                          -

                          GPU

                          概念

                          img

                          -

                          img

                          -

                          GPU体系结构

                          img

                          -

                          img

                          -

                          GPU计算

                          img

                          -

                          CUDA编程

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

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

                          -

                          img

                          -

                          img

                          -

                          下面两个标题反了额

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

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

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          例题

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          访存优化

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          真没懂。。。。

                          -

                          img

                          -

                          img

                          -

                          img

                          -

                          真没看懂

                          -

                          GPU控制流和指令优化

                          img

                          -

                          AI开发

                          DL框架

                          img

                          -

                          img

                          -

                          PyTorch

                          img

                          -

                          img

                          -

                          img

                          -

                          算子开发

                          img

                          -

                          img

                          -

                          TODO接下来有兴趣看吧

                          -

                          模型开发

                          img

                          -

                          04 自动驾驶体系结构

                          img

                          -

                          img

                          -

                          img

                          -

                          img

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

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

                          -

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

                          -

                          Trie

                          -

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

                          -

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

                          -

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

                          -

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

                          -

                          image-20230312150245669

                          -
                          -

                          心得

                          感想

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

                          -

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

                          -

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

                          -

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

                          -

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

                          -

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

                          -

                          错误集锦

                          sb错误

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

                          -

                          image-20230318154940622

                          -

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

                          -

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

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

                          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 -

                          如上是spring2023的代码模板。

                          -

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

                          -

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

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

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

                          -

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

                          -

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

                          -
                          错误使用unique_ptr::get

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

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

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

                          -
                          共享unique_ptr

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

                          -

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

                          -
                          代码规范

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

                          -
                          $ make format
                          $ make check-lint
                          $ make check-clang-tidy-p0
                          - -
                          自测

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

                          -

                          image-20230318165521340

                          -
                          In file included from /autograder/bustub/src/common/bustub_instance.cpp:17:
                          /autograder/bustub/src/include/common/bustub_instance.h:30:10: fatal error: 'libfort/lib/fort.hpp' file not found
                          #include "libfort/lib/fort.hpp"
                          - -

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

                          -

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

                          -

                          image-20230318173941576

                          -

                          image-20230318174102171

                          -

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

                          -

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

                          -

                          COW-Trie

                          -

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

                          -
                          -

                          先放个通关截图~

                          -

                          image-20230322235159843

                          -

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

                          -

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

                          -

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

                          -

                          Task1 cow-trie

                          -

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

                          -

                          下面举例说明

                          -

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

                          -

                          image-20230323000513767

                          -

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

                          -

                          image-20230323000601882

                          -

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

                          -

                          image-20230323000658620

                          -

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

                          -

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

                          -
                          -

                          感想

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

                          -

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

                          -

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

                          -

                          错误集锦

                          const转移

                          trie.h中:

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

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

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

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

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

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

                          -

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

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

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

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

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

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

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

                          -

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

                          -
                          -
                          -

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

                          -
                          -
                          make_shared

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

                          -
                          -

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

                          -
                          -
                          一个奇妙的报错

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

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

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

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

                          漏了个std::呃呃。

                          -
                          显式保存is_value_node_

                          trie.cpp RemoveHelper()中:

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

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

                          否则会:

                          -

                          image-20230323114435061

                          -

                          查看trie_test.cpp的代码:

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

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

                          ASSERT_EQ(trie.Get<uint32_t>("te"), nullptr);
                          ASSERT_EQ(trie.Get<uint32_t>("tes"), nullptr);
                          ASSERT_EQ(trie.Get<uint32_t>("test"), nullptr);
                          }
                          - -

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

                          -

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

                          -

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

                          -

                          image-20230323115902073

                          -

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

                          -

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

                          -

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

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

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

                          以及TrieNodeWithValue::Clone()

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

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

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

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

                          -

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

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

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

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

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

                          -

                          Task2 concurrency

                          -

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

                          -

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

                          -

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

                          -

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

                          -

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

                          -
                          -

                          感想

                          task2的内容是实现cow-trie并发安全版本的包装类TrieStore

                          -

                          相比于fall2022的并发内容,由于加上了cow的特性,本次实验更加复杂。我写了三版都没写对,看到别人的才豁然开朗(很遗憾没有自己再多想会儿……)接下来就从我的错误版本开始,逐步过渡到正确版本吧。

                          -

                          错误集锦

                          版本1

                          Get的实现很简单,按他说的一步步做就行,在这边不做赘述。PutRemove思路差不多,在此只放Put的代码。

                          -

                          image-20230321185244067

                          -

                          这样看起来很合理:同一时刻似乎确实只有一个writer对root_进行修改,也似乎确实同时可以有别的线程获取root_lock_对其进行读取。但其实,前者是错误的。

                          -

                          假如说进程A和进程B都在Put逻辑中。进程A执行到了root_ = new_trie这句话,然后进程B进入到root_.Put中。

                          -

                          root_ = new_trie使用了运算符=的默认实现,进行浅拷贝,故而会修改root_->root_root_.Put中会对root_->root_进行移动。

                          -

                          进程B在Put中执行std::move(root_)之后,进程A又让root_->root_变成了别的值(trie浅拷贝),导致原来的root_的引用计数变为0,自动释放(因为是智能指针shared_ptr),进程B在Put中再次访问就会寄。

                          -
                          -

                          注,此处是因为智能指针引用计数为零才释放的,cpp没有垃圾回收机制。

                          -
                          -
                          版本2
                          void TrieStore::Put(std::string_view key, T value) {
                          // You will need to ensure there is only one writer at a time. Think of how you can achieve this.
                          // The logic should be somehow similar to `TrieStore::Get`.
                          root_lock_.lock();
                          Trie tmp = root_;
                          root_lock_.unlock();

                          write_lock_.lock();
                          Trie new_trie = tmp.Put(key, std::move(value));
                          write_lock_.unlock();

                          root_lock_.lock();
                          root_ = new_trie;
                          root_lock_.unlock();
                          }
                          - -

                          版本1错误后,我发现我并没有按它强调的“somehow similar to Get”那样,模仿Get中的写法来做。于是我就修改了下,版本2诞生了。

                          -

                          但是这样的话,依然不能解决版本1中的问题。所以我又搞了个版本3.

                          -
                          版本3
                          void TrieStore::Put(std::string_view key, T value) {
                          write_lock_.lock();
                          Trie tmp = root_;
                          Trie new_trie = tmp.Put(key, std::move(value));
                          root_ = new_trie;
                          write_lock_.unlock();
                          }
                          - -

                          这样就能通过所有测试了。

                          -

                          但这样做虽然能解决多个writer的争夺问题,但不能解决一个writer和一个reader的争夺问题:因为两者都争夺同一个root_变量,但只有reader争夺root_lock_,这显然很不安全。因而,终极版本应该是这样:

                          -
                          正确版本
                          template <class T>
                          void TrieStore::Put(std::string_view key, T value) {
                          write_lock_.lock();
                          root_lock_.lock();
                          Trie tmp = root_;
                          root_lock_.unlock();

                          Trie new_trie = tmp.Put(key, std::move(value));

                          root_lock_.lock();
                          root_ = new_trie;
                          root_lock_.unlock();
                          write_lock_.unlock();
                          }
                          - - - -

                          可以看到整个思维过程是线性的,逐步改进下来,正确答案其实很容易想到。只可惜我太浮躁了,没有静下心来好好想,在版本3之后就去看了眼别人怎么写的(罪过)没有独立思考,算是一个小遗憾。

                          -

                          Task3 debugging

                          感想

                          一个考查我们debug入门技巧的小任务,简单,但我觉得形势很新颖。

                          -

                          debug过程

                          随便贴点debug过程的截图。

                          -

                          image-20230322221526353

                          -

                          image-20230322221634506

                          -

                          image-20230322221716462

                          -

                          小问题

                          gdb:Attempt to take address of value not located in memory.

                          任务中,需要获取root_的孙子。所以我就这么写了个gdb指令:p root_->children_.find('9')->second,然后就爆出了标题这个错误。

                          -

                          百度了下看到了这个:

                          -
                          -

                          gdb调试时好用的命令

                          -

                          image-20230323142137068

                          -
                          -

                          也许是因为我们通过.访问了children_的成员find吧(

                          -

                          总之,我最后是在trie_debug_test添加了这几行代码解决的:

                          -
                          // Put a breakpoint here.

                          // (1) How many children nodes are there on the root?
                          // Replace `CASE_1_YOUR_ANSWER` in `trie_answer.h` with the correct answer.
                          if (CASE_1_YOUR_ANSWER != Case1CorrectAnswer()) {
                          ASSERT_TRUE(false);
                          }
                          auto it = trie.root_->children_.find('9');
                          // (2) How many children nodes are there on the node of prefix `9`?
                          // Replace `CASE_2_YOUR_ANSWER` in `trie_answer.h` with the correct answer.
                          if (CASE_2_YOUR_ANSWER != Case2CorrectAnswer()) {
                          ASSERT_TRUE(false);
                          }
                          auto val = trie.Get<uint32_t>("93");
                          std::cout << val << it->first << std::endl;
                          // (3) What's the value for `93`?
                          // Replace `CASE_3_YOUR_ANSWER` in `trie_answer.h` with the correct answer.
                          if (CASE_3_YOUR_ANSWER != Case3CorrectAnswer()) {
                          ASSERT_TRUE(false);
                          }
                          - -

                          也即添加了it和val,以及防止unused报错的cout语句。gdb调试时打印it和val就行。

                          -
                          答案对但是过不了评测
                          -

                          来自CMU 15445 Project 0 (Spring 2023) 学习记录

                          -

                          在我本地的环境上,调试三问的答案分别是8 1 42,但该答案无法通过 Grade 平台的评测。发现在 Discord 上有人提出了同样的问题,助教 Alex Chi 给出了解答:

                          -

                          Alex Chi — 2023/02/15 23:29
                          It is possible that your environment produces different random numbers than the grading environment. In case your environment is producing different set of random numbers than our grader, replace your TrieDebugger test with:

                          -
                          -
                          auto trie = Trie();
                          trie = trie.Put<uint32_t>("65", 25);
                          trie = trie.Put<uint32_t>("61", 65);
                          trie = trie.Put<uint32_t>("82", 84);
                          trie = trie.Put<uint32_t>("2", 42);
                          trie = trie.Put<uint32_t>("16", 67);
                          trie = trie.Put<uint32_t>("94", 53);
                          trie = trie.Put<uint32_t>("20", 35);
                          trie = trie.Put<uint32_t>("3", 57);
                          trie = trie.Put<uint32_t>("93", 30);
                          trie = trie.Put<uint32_t>("75", 29);
                          -
                          -

                          难绷,我反复确认了好几遍(。主要还是太相信cmu的权威了,觉得这实验都发布了好几个月了应该不会有错,就没想到是这个问题。我觉得最好还是把这个问题反应在指导书上吧。

                          -

                          Task4 SQL String Functions

                          -

                          Now it is time to dive into BusTub itself!

                          -

                          You will need to implement upper and lower SQL functions.

                          -

                          This can be done in 2 steps:

                          -
                            -
                          1. implement the function logic in string_expression.h.
                          2. -
                          3. register the function in BusTub, so that the SQL framework can call your function when the user executes a SQL, in plan_func_call.cpp.
                          4. -
                          -

                          To test your implementation, you can use bustub-shell:

                          -
                          cd build
                          make -j`nproc` shell
                          ./bin/bustub-shell
                          bustub> select upper('AbCd'), lower('AbCd');
                          ABCD abcd
                          -
                          -

                          感想

                          说实话乍一看我还没看懂(。它放在这个位置,我还以为跟上面实现的cow-trie有什么关系,并且误以为这个upper和lower是什么上层接口底层接口的意思,跟它大眼瞪小眼了半天。直到看到了下面的案例,才发现跟trie似乎没有任何关系23333

                          -

                          本次实验内容其实就是实现sql的转换大小写的函数。知道了要做什么之后,任务就很简单了,按着它提示一步步做就行。

                          -

                          不过此task重点其实也是在稍微了解下我们接下来要打交道的sql框架的代码。比如说,此次我们的实现涉及到的,居然是一个差不多是工厂模式(其实更像策略模式?)的一部分:

                          -

                          外界传入想调用的函数名,通过GetFuncCallFromFactory获取对应的处理对象

                          -

                          image-20230322154915206

                          -

                          得到处理对象后调用其Compute方法就行

                          -

                          image-20230322154849449

                          -

                          第一次如此鲜明地看到一个设计模式在cpp的应用,真是让我非常震撼。

                          -

                          代码规范

                          依旧是这三件套:

                          -
                          make format
                          make check-lint
                          make check-clang-tidy-p0
                          - -

                          错误集锦

                          关于sanitizer

                          执行了该命令:cmake -DCMAKE_BUILD_TYPE=Debug -DBUSTUB_SANITIZER= ..之后,执行make报错missing argument to '-fsanitize='

                          -

                          发生这个的原因是cmake的命令中将BUSTUB_SANITIZER设置成了空。解决方法就是将其设置为别的值就好了,具体想设置成什么值可以参见:关于GCC/LLVM编译器中的sanitize选项用处用法详解 我这里姑且随便设置了个leak

                          -]]>
                          -
                          - - Project2 B+Tree - /2023/03/13/cmu15445$lab2/ - -

                          参考

                          -

                          CMU 15-445 Project 2 (Spring 2023) | 关于 B+Tree 的十个问题

                          -

                          对crabbing lock、乐观锁做了详尽解释

                          - -

                          Project2 B+Tree

                          -

                          In this programming project you will implement a B+Tree index in your database system.

                          -

                          Your implementation will support thread-safe search, insertion, deletion (including splitting and merging nodes包括分裂和合并结点), and an iterator to support in-order leaf scans.

                          +

                          关联的另一个实例:

                          +

                          image-20231117015508123

                          +

                          此时由于T’.inh还要被a3用到,所以我们就得在T’出栈前把它的这个inh值复制给a3。

                          -

                          它这里的B+树(以及wiki里的)跟王道考研讲得不大一样。王道考研的B+每个结点一个关键字对应一个child,但是这里的是B树的形式。

                          -

                          undefined

                          -

                          image-20230417181239196

                          -

                          Task1 B+Tree Pages

                          -

                          You must implement three Page classes to store the data of your B+Tree:

                          +

                          当遇到语义动作之后,就执行动作,并且出栈语义动作。

                          +

                          picture

                          +

                          它这意思应该是遇到每个产生式的每个符号要执行什么动作都是确定的,所以代码实现是可能的。

                          +

                          可以看到:

                            -
                          1. B+Tree Page BPlusTreePage

                            -

                            下面那两个的基类

                            -
                          2. -
                          3. B+Tree Internal Page

                            -

                            An Internal Page stores m ordered keys and m+1 child pointers (as page_ids) to other B+Tree Pages.These keys and pointers are internally represented as an array of key/page_id pairs.

                            -

                            Because the number of pointers does not equal the number of keys, the first key is set to be invalid, and lookups should always start with the second key.

                            -

                            At any time, each internal page should be at least half full.【min_size<= <=max_size】

                            -

                            During deletion, two half-full pages can be merged, or keys and pointers can be redistributed to avoid merging. During insertion, one full page can be split into two, or keys and pointers can be redistributed to avoid splitting.

                            -
                          4. -
                          5. B+Tree Leaf Page

                            -

                            The Leaf Page stores m ordered keys and their m corresponding values. In your implementation, the value should always be the 64-bit record_id for where the actual tuples are stored; see the RID class, in src/include/common/rid.h.

                            -

                            *Note:* Even though Leaf Pages and Internal Pages contain the same type of key, they may have different value types. Thus, the max_size can be different.

                            -
                          6. +
                          7. 语义动作代码就是执行
                          8. +
                          9. 综合属性代码就是赋给关联语义动作
                          10. +
                          11. 非终结符就是选一个它作为左部的产生式,然后看看要不要用到它自身的属性对右部子属性进行复制(体现了继承属性)
                          +

                          递归的预测分析

                          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的文章 - 知乎

                          -

                          这个没什么好说,大概就是有一个基类结点,它有两个子类,一个表示b+树的leaf node,另一个表示b+树的internal node,每个结点都占据一个内存页。嗯写就是了。

                          -

                          Task2a Insertion and Search + Task3 Iterator

                          -

                          The index should support only unique keys; if you try to reinsert an existing key into the index, it should not perform the insertion, and should return false. key必须unique

                          -

                          B+Tree pages should be split (or keys should be redistributed) if an insertion would violate the B+Tree’s invariants. 插入时需要分裂

                          -

                          If an insertion changes the page ID of the root, you must update the root_page_id in the B+Tree index’s header page. You can do this by accessing the header_page_id_ page, which is given to you in the constructor. Then, by using reinterpret_cast, you can interpret this page as a BPlusTreeHeaderPage (from src/include/storage/page/b_plus_tree_header_page.h) and update the root page ID from there. You also must implement GetRootPageId, which currently returns 0 by default.对root_page_id的一切访问,都需要通过header_page_id_。如果插入后改变了root的page ID,需要更新root_page_id

                          -

                          We recommend that you use the page guard classes from Project 1 to help prevent synchronization problems. For this checkpoint, we recommend that you use FetchPageBasic (defined in src/include/storage/page/) when you access a page. 在当前task中,我们推荐你使用pro1实现的page guard,比如说这里如果要访问一页,就需要用 FetchPageBasic

                          -

                          You may optionally use the Context class (defined in src/include/storage/index/b_plus_tree.h) to track the pages that you’ve read or written (via the read_set_ and write_set_ fields) or to store other metadata that you need to pass into other functions recursively.你可以随意使用和修改 Context class,它大概就是一个存储共享信息的对象。

                          -

                          If you are using the Context class, here are some tips:如果你要用,要注意以下几点:

                          -
                            -
                          • You might only need to use write_set_ when inserting or deleting. 当你在为B+树插入/删除结点时,需要用到write_set_。【为什么?这个set存储的是修改路径上的结点吗?然后如果要分裂/合并结点,只需什么while(pop且需要分裂/合并){分裂/合并}??所以说这里的deque是栈结构?】

                            -

                            也就是说,其实我们就可以不用递归了,而是将上下文存储在context->write_set_这个栈里面就行了?大概是这个意思吧

                            -

                            It is possible that you don’t need to use read_set_, depending on your implementation.

                            -

                            read可以用递归(比较简单)也可以不用,所以说具体看实现。

                            -
                          • -
                          • You might want to store the root page id in the context and acquire write guard of header page when modifying the B+Tree.你需要将root page id存储在context,并且在修改b+树(插入、删除)时获取header page的WritePageGurad。

                            -
                          • -
                          • To find a parent to the current node, look at the back of write_set_. It should contain all nodes along the access path.如果想要寻找当前node的父亲,可以看看write_set_.back,它包含了访问路径上所有结点的引用【所以确实是当成栈来用了】

                            -
                          • -
                          • You should use BUSTUB_ASSERT to help you find inconsistent data in your implementation. 需要使用 BUSTUB_ASSERT

                            -

                            For example, if you want to split a node (except root), you might want to ensure there is still at least one node in the write_set_. If you need to split root, you might want to check if header_page_ is std::nullopt.

                            -

                            如果你想要分割一个根节点以外的node,那你必须保证write_set_中至少有一个结点;如果你想要分割根节点,那你必须保证header_page_非空。

                            -
                          • -
                          • To unlock the header page, simply set header_page_ to std::nullopt. To unlock other pages, pop from the write_set_ and drop.如果你想要不锁住header page,那就置其为空指针;如果想释放别的页,那就将它从 write_set_ pop出来就行。【这是因为我们要用到的page类型都是page guard,可以析构时unpin吗?】

                            -
                          • -
                          -
                          -

                          感想

                          由于各种原因,lab2的战线还是拉得太长了。四月份完成了代码初版,中间修了几个bug勉强通过了insertion test,然后一直到十一月底的现在才再次捡起来。不得不说,回看当初的代码,还是能够很清晰地感受到自己这半年多来的成长的,令人感慨。

                          -

                          我先是花了一天的时间重构了下以前写的所有代码,然后再花了两天时间修bug终于通过了insertion test和sequence scale test,并且将b+树的代码修到了我满意的地步(指不像以前那样一坨重复代码和中文注释。。。)。

                          -

                          思路

                          这里简要介绍下B+树的插入实现及我觉得实现中需要注意的几个要点吧。

                          -

                          B+树的插入流程大概是这样的:

                          -
                            -
                          1. 查找到key要插入的叶子结点(途中需要维护write_set,也即查找路径)

                            -
                          2. -
                          3. 判断结点是否满

                            -
                              -
                            1. 未满,直接插入即可。(我采取插入排序的方法)

                              -
                            2. -
                            3. 已满,需要对结点进行分裂。

                              -

                              推举出中间结点tmp_key,它和新结点page_id接下来将插入到父节点中。

                              -
                            4. -
                            -
                          4. -
                          5. 持续进行分裂:

                            -

                            需要注意具体的分裂方法,我认为其中internal page size == 3的情况尤为棘手。在具体实现中,我是这样分裂的:

                            -
                              -
                            1. 推举出将要被插入到父节点的tmp_key

                              -

                              该推举出的key将不会出现在分裂后的新旧结点中,而是会被加入到父节点中。默认为(m + 1) / 2【m为max size】。

                              -

                              但是要尤其注意size为3的case,此时tmp_key为array_[2],很有可能右边结点为空。所以我们需要做点特殊处理:

                              +
                              +

                              考虑一个问题:给出若干个模式串,如何构建一个DFA,接受所有以任一模式串结尾(称为与该模式串匹配)的文本串?

                              +

                              可以先思考一个更简单的问题:如何构建接受所有模式串的DFA?很明显,**字典树**就可以看做符合要求的自动机。例如,有模式串"abab""abc""bca""cc" ,我们把它们插入字典树,可以得到:

                              +

                              picture

                              +

                              为了使它不仅接受模式串,还接受以模式串结尾的文本串,一个看起来挺正确的改动是,使每个状态接受所有原先不能接受的字符,转移到初始状态(即根节点)。

                              +

                              picture

                              +

                              但是如果我们尝试"abca",我们会发现我们的自动机并不能接受它。稍加观察发现,我们在状态5接受a应该跳到状态8才对,而不是初始状态。某种意义上来说,状态7是状态5退而求其次的选择,因为状态7在trie上对应的字符串"bc"是状态5对应的字符串"abc"后缀。既然状态5原本不能接受"a",我们完全可以退而求其次看看状态7是否可以接受。这看起来很像KMP算法,确实,AC自动机常常被人称作trie上KMP。

                              +

                              所以我们给每个状态分配一条fail边,它连向的是该状态对应字符串在trie上存在的最长真后缀所对应的状态。我们令所有状态p接受原来不能接受的字符c,转移到 next(fail(p),c) ,特别地,根节点转移到自己。为什么不需要像KMP算法一样,用一个循环不断进行退而求其次的选择呢?因为如果我们用BFS的方式进行上面的重构,我们可以保证 fail(p) 在p重构前已经重构完成了,类似于动态规划

                              +

                              picture

                              +

                              这样建fail边和重构完成后得到的自动机称为AC自动机(Aho-Corasick Automation)。

                              +

                              我们发现fail边也形成一棵树,所以其实AC自动机包含两棵树:trie树fail树。一个重要的性质是,如果当前状态 p 在某个终止状态 s 的fail树的子树上,那么当前文本串就与 s 所对应模式串匹配

                              +
                              +

                              也就是说它的解决方法是加fall边(蓝色)和加新边(红色),

                              +]]> + + + JavaWeb + /2022/12/21/JavaWeb/ + 第一部分 Java基础

                              JUnit单元测试

                              JUnit是白盒测试。

                              +

                              简要使用步骤

                              定义测试类

                              包含各种测试用例。

                              +

                              一般放在包名xxx.xxx.xx.test里,类名为“被测试类名Test”。

                              +

                              定义测试方法

                              测试方法可以独立运行。

                              +

                              方法名一般为“test测试的方法”,void,空参。

                              +

                              给方法加@Test标签

                              加入JUnit依赖包

                              具体细节

                              断言

                              Assert.assertEquals(3,result);
                              + +

                              @Before @After

                              @Before在所有测试方法执行前自动执行,常用于资源申请。

                              +

                              @After在所有测试方法执行完后自动执行,常用于释放资源。

                              +

                              反射

                              反射是框架设计的灵魂。

                              +

                              Java对象创建的三个阶段

                              image-20221205194807395

                              +

                              类加载器把硬盘中的字节流文件装载进内存,并且翻译封装为Class类对象。通过Class类对象才能创建Person对象。

                              +

                              而这也就是说,如果我们有了Class对象,我们就可以创建该类对象。

                              +

                              获取Class对象

                              有三种方式。

                              +

                              Class.forName(“类的全名”)

                              将字节码文件加载进内存,返回class对象。多用于配置文件【将类名定义在配置文件】

                              +

                              注意:类的全名指的是包.类,包含包名。

                              +

                              类型.class

                              通过类名的属性class获取。多用于参数传递。

                              +

                              对象.getClass()

                              getClass()是Object类的方法。多用于对象的获取字节码的方式。

                              +
                              //第一种方式
                              Class class1 = Class.forName("Student");
                              System.out.println(class1);
                              //第二种方式
                              Class class2 = Student.class;
                              System.out.println(class2);
                              //第三种方式
                              Student stu = new Student();
                              Class class3 = stu.getClass();
                              System.out.println(class3);

                              System.out.println((class1==class2)+" "+(class2==class3));

                              /*输出
                              class Student
                              class Student
                              class Student
                              true true*/
                              + +

                              同一个字节码文件(*.class)在一次程序运行过程中只会被加载一次,不管是以哪种方式得到的Class对象,都是同一个。

                              +

                              使用Class对象

                              可以通过class对象得到其字段、构造方法、方法等。

                              +
                              class Student{
                              public String name;
                              int birthday;
                              protected int money;
                              private double weight;

                              private Student() {

                              }
                              public Student(String name, int birthday, int money, double weight) {
                              this.name = name;
                              this.birthday = birthday;
                              this.money = money;
                              this.weight = weight;
                              }

                              @Override
                              public String toString() {
                              return "Student{" +
                              "name='" + name + '\'' +
                              ", birthday=" + birthday +
                              ", money=" + money +
                              ", weight=" + weight +
                              '}';
                              }

                              public static void haha(){
                              System.out.println("haha");
                              }
                              public void hello(){
                              System.out.println("hello");
                              }
                              public void hello(String name){
                              System.out.println("hello,"+name+"!");
                              }
                              private void giveMoney(){
                              System.out.println("my money is yours...");
                              money = 0;
                              }
                              }
                              + +
                              Student stu = new Student("张三",321,1000,57.7);
                              Class stuC = stu.getClass();
                              + +

                              字段

                              获取字段

                              常用方法:

                              +
                              //获取所有公有字段
                              Field[] fs = stuC.getFields();
                              //获取某个公有字段
                              Filed f = stuC.getField("name");
                              //获取所有字段
                              Field[] fs = stuC.getDeclaredFields();
                              //获取某个字段
                              Filed f = stuC.getDeclaredField("name");
                              /*输出
                              public java.lang.String Student.name

                              public java.lang.String Student.name

                              public java.lang.String Student.name
                              int Student.birthday
                              protected int Student.money
                              private double Student.weight*/
                              + +
                              使用字段
                              Field f = stuC.getDeclaredField("money");
                              //由于money protected,故应该先设置其为可访问,否则会抛出异常。
                              //这是暴力反射,不推荐。
                              f.setAccessible(true);
                              //获取成员变量的值
                              //注意此处是要用field.get(该类型对象)的。想想也有道理,Filed字段是属于Class对象的,因而你想获取某个对象的值当然得传入该对象。
                              System.out.println(f.get(stu));
                              //设置对象的值
                              f.set(stu,0);
                              System.out.println(stu);
                              /* Student{name='张三', birthday=321, money=0, weight=57.7} */
                              + +

                              构造方法

                              获取方法跟上面格式差不多。

                              +
                              //获取构造方法
                              //获取无参私有构造方法
                              Constructor cons = stuC.getDeclaredConstructor();
                              cons.setAccessible(true);
                              System.out.println(cons.newInstance());
                              //获取有参构造方法
                              Constructor cons2 = stuC.getConstructor(String.class,int.class,int.class,double.class);
                              System.out.println(cons2.newInstance("李四",102,100,90.9));
                              /*输出
                              Student{name='null', birthday=0, money=0, weight=0.0}
                              Student{name='李四', birthday=102, money=100, weight=90.9}*/
                              + +

                              如果想要获取公有的无参构造器,还可以使用Class类提供的更简单的方法,不用先创造构造器:

                              +
                              System.out.println(stuC.newInstance());
                              + +

                              方法

                              //获取方法
                              //可以获取静态方法
                              Method m = stuC.getMethod("haha");
                              System.out.println(m);
                              //获取带参方法,自动根据参数推断
                              Method m2 = stuC.getMethod("hello");
                              Method m3 = stuC.getMethod("hello",String.class);
                              //调用方法
                              m2.invoke(stu);
                              m3.invoke(stu,"琳琳");
                              //获取私有方法并调用
                              Method m4 = stuC.getDeclaredMethod("giveMoney");
                              m4.setAccessible(true);
                              m4.invoke(stu);
                              /*输出
                              public static void Student.haha()
                              hello
                              hello,琳琳!
                              my money is yours...*/
                              + +

                              使用反射的案例

                              image-20221205205122805

                              +
                              public class ReflectTest {
                              public static void main(String[] args) throws Exception {
                              /* 加载配置文件 */
                              Properties pro = new Properties();
                              //获取字节码文件加载器
                              ClassLoader cl = ReflectTest.class.getClassLoader();
                              pro.load(cl.getResourceAsStream("pro.properties"));

                              /* 获取配置文件中的数据并执行 */
                              Class cla = Class.forName(pro.getProperty("className"));
                              cla.getMethod(pro.getProperty("methodName")).invoke(cla.newInstance());
                              }
                              }
                              + + + +

                              注解

                              image-20221205211932790

                              +

                              image-20221205212028989

                              +

                              ①和③都是jdk预定义的。自定义主要是②。

                              +

                              生成doc文档

                              javadoc XXX.java
                              + +

                              会自动根据里面的注解生成文档

                              +

                              JDK预定义注解

                              image-20221205212443918

                              +

                              自定义注解

                              注解类的本质

                              @Target(ElementType.METHOD)
                              @Retention(RetentionPolicy.SOURCE)
                              public @interface Override {
                              }
                              + +

                              本质上

                              +
                              public @interface Override{}
                              + +

                              等价于

                              +
                              public interface Override extends java.lang.annotation.Annotation{}
                              + +

                              注解的属性

                              注解的属性就是接口中的成员方法。要求无参,且返回类型有固定取值:

                              +

                              image-20221205213135522

                              +
                              public @interface MyAnno {
                              String name() default "haha";
                              }
                              + +
                              @MyAnno(name = "haha")
                              public class Student{}
                              + +

                              元注解

                              描述注解的注解

                              +

                              image-20221205213324052

                              +

                              RetentionPolicy的三个取值:SOURCE、CLASS、RUNTIME,正对应着java对象的三个阶段。

                              +

                              SOURCE:不保留到字节码文件,会被编译器扔掉

                              +

                              CLASS:保留到字节码文件

                              +

                              RUNTIME:被读到

                              +

                              自定义的注解一般都取RUNTIME

                              +

                              在程序中获取注解属性

                              相当于用注解替换配置文件

                              +
                              @Target(ElementType.TYPE)
                              @Retention(RetentionPolicy.RUNTIME)
                              public @interface Pro {
                              String className();
                              String methodName();
                              }
                              //保留在runtime应该是因为运行时要动态获取值。我试了一下换成CLASS或者SOURCE,会有NullPointerException
                              + +
                              @Pro(className = "Student",methodName = "hello")
                              public class ReflectTest {
                              public static void main(String[] args) throws Exception{
                              Pro pro = ReflectTest.class.getAnnotation(Pro.class);

                              Class cla = Class.forName(pro.className());
                              cla.getMethod(pro.methodName()).invoke(cla.newInstance());
                              }
                              }
                              + +

                              class.getAnnotation(Pro.class);这句话实质上是创建了一个实例,继承了Pro接口,重载了里面的抽象方法。

                              +

                              使用案例:测试框架

                              @Target(ElementType.METHOD)
                              @Retention(RetentionPolicy.RUNTIME)
                              public @interface Check {
                              }
                              + +

                              然后在要测试的每个方法上面加上此标签。

                              +

                              image-20221205215017285

                              +

                              然后编写test方法:

                              +
                              public class TestCheck {
                              public static void main(String[] args) throws IOException {
                              //创建对象
                              Calculator c = new Calculator();
                              //获取所有方法
                              Method[] methods = c.getClass().getMethods();

                              //写入文件
                              int number = 0;//异常次数
                              BufferedWriter bf = new BufferedWriter(new FileWriter("bug.txt"));

                              //检查每个方法是否有注解。有的话则执行。
                              for (Method m : methods){
                              if (m.isAnnotationPresent(Check.class)){
                              try {
                              m.invoke(c);
                              } catch (Exception e) {
                              //记录文件信息
                              number++;
                              bf.write(m.getName()+"出异常了。");
                              bf.write("\n");
                              bf.write(e.getCause().getClass().getSimpleName()+" "+e.getCause().getMessage());
                              bf.write("\n");
                              bf.write("-------------");
                              bf.write("\n");
                              }
                              }
                              }
                              bf.write("共出现"+number+"次异常");
                              bf.flush();
                              bf.close();
                              }
                              }
                              /*
                              haha出异常了。
                              ArithmeticException / by zero
                              -------------
                              共出现1次异常
                              */
                              + +

                              image-20221205220053258

                              +

                              第二部分 数据库

                              Mysql

                              登录方式

                              mysql -h[IP地址] -u[用户名] -p
                              + +

                              文件结构

                              本地的一个文件夹就代表一个数据库,文件夹里的一个文件代表一张表。

                              +

                              image-20221205221041508

                              +

                              image-20221205221055114

                              +

                              SQL语法

                              image-20221205221216882

                              +

                              SQL有四种语句类型

                              +

                              image-20221205221246672

                              +

                              DDL 操作数据库、表

                              操纵数据库

                              create datebase 数据库名称;
                              create datebase if not exists 数据库名称;
                              create datebase if not exists 数据库名称 character set gbk;
                              + +
                              drop database 数据库名称;
                              drop database if exists 数据库名称;
                              + +
                              alter database 数据库名称 charactor set 修改后新值;
                              + +
                              show databases;# 查询所有数据库名称
                              show create database 数据库名称;# 显示指定数据库创建时的指令内容
                              + +
                              使用
                              select database();# 查询正在使用的数据库名称
                              use 数据库名称;
                              + +

                              操纵表

                              create table students(
                              name varchar(20),
                              age int,
                              score double(3,1),
                              birthday date,
                              insert_time timestamp
                              );# 创建表
                              create table students2 like students;# 复制表
                              + +

                              *注:

                                -
                              1. 当要插入到该节点的insert_key > array_[(m + 1) / 2]时,我们推举(m + 1) / 2这个结点。
                              2. -
                              3. insert_key < array_[m / 2],我们转而推举m / 2(此时为array_[1])。
                              4. -
                              5. insert_key < array_[(m + 1) / 2]insert_key > array_[m / 2]时,我们应该对此做出特殊处理,推举insert_key。在此为了代码实现方便,我们还需要调换insert_key和tmp_key的地位
                              6. -
                              -
                              special = false;
                              middle = (m + 1) / 2;
                              tmp_key = root->KeyAt(middle);
                              insert_small_than_tmp_key = (comparator_(insert_key, tmp_key) < 0);
                              if (insert_small_than_tmp_key) {
                              middle = m / 2;
                              tmp_key = root->KeyAt(middle);
                              if (comparator_(insert_key, tmp_key) >= 0) {
                              special = true;
                              swap = insert_key;
                              insert_key = tmp_key;
                              tmp_key = swap;
                              }
                              }
                            2. -
                            3. 分裂旧结点

                              -

                              被推举出的tmp_key的value及其右部元素会变成新结点,左部依然留在旧结点,tmp_key会到父节点中去。也即如下图所示:

                              -

                              ![未命名文件 (1)](./cmu15445/未命名文件 (1).png)

                              -

                              依然是注意上面那个case3特殊情况,需要交换insert key和middle key:

                              -
                              if (!special)
                              new_page->SetValueAt(0, root->ValueAt(middle));
                              else {
                              new_page->SetValueAt(0, insert_val);
                              insert_val = root->ValueAt(middle);
                              }
                            4. -
                            5. 持续进行推举和分裂,直到父节点不用分裂

                              -

                              此时直接将insert key和insert value插入排序到父节点即可。

                              -
                            6. -
                            +
                          6. mysql的数据类型表

                            +

                            image-20221205222127740

                            +

                            其中:

                            +

                            ① double(3,1)表示XXX.X,最大值为99.9.

                            +

                            ② 关于三个时间类型

                            +

                            image-20221205222307284

                            +

                            所以timestamp常用作插入时间。

                            +

                            ③ varchar(20)表示二十个字符长的字符串。

                            +

                            注意,是“二十个字符”而不是“二十个字节”。如果使用的字符集每个字符占3个字节,则varchar(20)占60个字节。

                            +

                            ④ BLOB、CLOB、二进制这些用于存储大数,不常用

                          -

                          然后是Iterator的话,我感觉这也是设计得很不错,让我们亲手写了下c++的重载运算符,也是让我学到了很多c++知识。。。

                          -

                          遇到的问题

                          感觉问题其实不多,主要还是debug有点痛苦花了很长时间()

                          -

                          cmake报错

                          切换内核前后报错。

                          -

                          Check for working C compiler: /usr/bin/cc - broken

                          -

                          感觉可能是内核切来切去,导致cmake cache发生了点小问题?总之我最后在5.11内核把build文件删了,重新执行cmake -DCMAKE_CXX_COMPILER=$(which g++) -DCMAKE_C_COMPILER=$(which gcc) ..就ok了。

                          -

                          page guard

                          用错了

                          image-20230505002652748

                          -

                          我发现在这里创建的root最后好像会被释放掉?

                          -

                          比如我看到新root的page为6,连接也做得好好的,最后出了函数就寄了:

                          -

                          image-20230505002731312

                          -

                          还有一个是发现新的leaf page好像不大对,其类型甚至是internal呃呃,我调下看看

                          -

                          尼玛,绷不住了是这里:

                          -

                          image-20230505011731797

                          -

                          原来写的

                          -

                          image-20230505011744924

                          -

                          改了之后test2马上ok,乐

                          -
                          作用域

                          还弄了个commit修:

                          -

                          image-20231130222702358

                          -

                          一点c++引用震撼

                          auto INDEXITERATOR_TYPE::operator*() -> const MappingType &
                          +
                          drop table 表名;
                          -

                          这个函数卡了我还挺久。。。里面逻辑很简单,不过难就难在怎么构造出一个const MappingType &

                          -

                          如果这样:

                          -
                          INDEX_TEMPLATE_ARGUMENTS
                          auto INDEXITERATOR_TYPE::operator*() -> const MappingType & {
                          auto page = guard_.As<LeafPage>();
                          return std::pair<KeyType, ValueType>(page->KeyAt(cnt_), page->ValueAt(cnt_));
                          // or use make_pair. the same result
                          }
                          +
                          # 修改表名
                          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;
                          -

                          会说你临时对象不能作为引用。如果这样:

                          -
                          INDEX_TEMPLATE_ARGUMENTS
                          auto INDEXITERATOR_TYPE::operator*() -> const MappingType & {
                          auto page = guard_.As<LeafPage>();
                          auto res = new MappingType(std::pair<KeyType, ValueType>(page->KeyAt(cnt_), page->ValueAt(cnt_)));
                          return *res;
                          }
                          +
                          show tables;# 查询数据库中所有表的名字
                          desc 表名;# 查询某个表的结构
                          -

                          又会找不到机会delete导致内存泄漏。冥思苦想了半天不知道该怎么办,最后从网上看了别人怎么写的:

                          -
                          INDEX_TEMPLATE_ARGUMENTS
                          auto INDEXITERATOR_TYPE::operator*() -> const MappingType & {
                          auto page = guard_.As<LeafPage>();
                          return page->PairAt(cnt_);
                          }

                          INDEX_TEMPLATE_ARGUMENTS
                          auto B_PLUS_TREE_LEAF_PAGE_TYPE::PairAt(int index) const -> const MappingType & {
                          return array_[index];
                          }
                          -

                          我服了。

                          -

                          不过可能有更好的解决方法?可惜我c++水平不大够,所以暂时想不出来了。

                          -

                          Task4 Remove

                          感想

                          由于有了insert的沉淀,remove的实现便相较不大困难了,写完代码到通过内置的delete测试只花了一天的时间。

                          -

                          思路

                            -
                          1. 找到需要操作的叶结点路径

                            + +

                            DML 增删改表中数据

                            insert into students(name,age,score,birthday) values('张三',15,99.9,"2022-12-5");
                            insert into students values("张三",15,99.9,"2022-12-5",NULL);
                            + +

                            如果不加条件,会把表中所有数据删除

                            +
                            delete from students where name="张三";
                            truncate table students;# 删除表,然后创建一张一模一样的新表
                            + +

                            image-20221205224600869

                            +

                            如果不加条件,会把表中所有记录全部修改

                            +
                            update students set name="1", age=10 where name="张三";
                            + + + +

                            DQL 查询表中记录

                            语法

                            image-20221205224802814

                            +

                            基础查询

                            select # 多字段查询
                            name,
                            age
                            from
                            students;

                            select distinct # 去重
                            address
                            from
                            students;

                            # 有NULL参与的计算结果都为NULL
                            select name,math,english,math+english from students;
                            # ifnull函数不会修改原表中的数据
                            select name,math,english,IFNULL(math,0)+IFNULL(english,0) from students;

                            select
                            name,
                            math,
                            english,
                            IFNULL(math,0)+IFNULL(english,0) total_score # 起别名
                            from
                            students;
                            + +

                            条件查询

                            运算符
                              +
                            1. 基本运算符

                              +

                              <、>、=、<=、>=、<>(不等于,也可以用!=)

                            2. -
                            3. 判断叶子结点属于以下四种策略中的哪一种,执行对应策略(优先级从高到低):

                              -
                                -
                              1. 直接删除

                                -

                                当删除后叶结点元素数仍在合法范围,并且路径上父节点没有target key,直接删除然后返回即可。

                                +
                              2. 逻辑运算符

                                +

                                AND、OR

                              3. -
                              4. 更新父节点路径

                                -

                                当删除后叶结点元素数仍在合法范围,并且路径上父节点target key,直接删除然后向上回溯更新父节点即可。

                                +
                              5. BETWEEN AND

                              6. -
                              7. 窃取兄弟元素

                                -
                                If do a steal, we should update related key in the parent, and update up till reaching the root.

                                /*
                                For that steal is more simple, we first check whether it can do a steal first.
                                We steal the node whose size is biggest between the next and the prev node.
                                If the prev size is bigger, we only update self key in parent.
                                If the next size is bigger, we update both self key and next key n parent.
                                After that, we trace back and update all the parent nodes which contains the
                                target key.
                                */
                                - -

                                当删除后叶结点元素数过少,并且左右兄弟元素充足,则从左右兄弟窃取一个。优先窃取元素最多者。

                                -
                                  -
                                1. 窃取左兄弟

                                  -

                                  窃取左兄弟的最大元素

                                  -

                                  需递归更新自身父节点路径上的对应值。

                                  +
                                2. IN后跟集合

                                  +

                                  image-20221205230824086

                                3. -
                                4. 窃取右兄弟

                                  -

                                  窃取右兄弟的最小元素

                                  -

                                  需要递归更新自身和右兄弟父节点路径上的对应值。

                                  +
                                5. IS、IS NOT

                                  +

                                  image-20221205230902110

                                6. +
                                7. LIKE 模糊查询

                                  +

                                  类似正则使用占位符匹配

                                  +

                                  image-20221205231037021

                                  +
                                  select * from students where name like "马%";
                                -

                                之后返回即可。

                                -
                              8. -
                              9. 合并

                                -
                                /*
                                Need to merge with one of the node. It is more simple to try to merge the left node
                                first. So the strategy:
                                1. Pick the prev node to merge. (If leaf is most left, pick next node)
                                2. Update delete-key. (for prev, it's leaf[0]; for next, it's right key, and need
                                to update self)
                                3. Go up till reaching root. Do:
                                1. delete delete-key.
                                2. pick merging or stealing like above.
                                1. if merge, update delete-key, go up;
                                2. if steal, break to do update and has no need to go up.
                                4. Remember to deal with edge case: root.
                                */
                                +

                                各种函数一样的东西

                                排序函数
                                order by 排序字段1 排序方式1,排序字段2 排序方式2;
                                -

                                当删除后叶结点元素数过少,并且左右兄弟元素也都是最小值,那么需要与左右兄弟之一进行合并。优先合并左兄弟。合并都为大->小,也即target->左兄弟 或者 右兄弟->target。

                                -

                                需要递归删除父节点路径上的merge from元素。

                                +
                                  +
                                1. 默认升序。

                                2. -
                                +
                              10. ASC、DESC

                              11. -
                              12. 可以看到,1/2/3三种情况都可以实现简单地直接返回。4稍显复杂,由于递归删除,所以需要对每一个父节点都再次进行上面几种策略的判断,直到遇到情况123返回为止。

                                +
                              13. 多关键字排序

                                +

                                image-20221205231606255

                                +

                                第二条件仅当第一条件一样才使用。

                              -

                              遇到的问题

                              一个比较sb的小bug……

                              -

                              image-20231204163052528

                              -

                              Task5 Concurrency

                              感想

                              这位可更是重量级,足足花了我三天的时间……不过感觉第一次处理这么一个复杂的并发情景,花的时间还是值得的。

                              -

                              最后的结果虽然很一般(指排行榜倒数水平。。。),但至少还是过了。就先这样吧。

                              -

                              image-20231204170030245

                              -

                              思路

                              我实现了crabbing lock+optimal lock。对于Insert和Remove,都是先在一开始获取header page和路径上父节点的读锁,然后在之后有可能向上更新时(比如说Insert的需要分裂、Remove的Update和Merge两种情况),丢弃所有读锁,然后获取header page和路径上父节点的写锁。

                              -

                              不过感觉我这个思路还是略有粗糙,因为相当一部分时间都得占用header page的写锁。但是我思考了一下细粒度方案,发现还是有点难实现。比如说,对于insert,细粒度化的方式也许就是一直持有header page的读锁,一直到需要分裂根节点时,才释放读锁获取写锁。但这样一来就会暴露一个危险的空窗期(而且感觉这个空窗期还不小),当你真的拿到写锁,这树的结构可能已经变得不知道什么样了。在这种情况下,你就需要再做一次回溯工作,也即获取从新root结点到旧root结点的路径,递归插入insert key和insert value,最后安全分裂根节点(因为此时已经安全持有了header page写锁)。感觉思路也是比较易懂,但是实现上还是太麻烦了,所以先暂且搁置吧。

                              -

                              遇到的问题

                              这种感觉大多还是在面向测试用例见招拆招……所以其实感觉没什么好说的。

                              -

                              bpm遗留

                              这个并发问题是这样的,我原来是先evict,然后再写回被替换的页面,写回过程中磁盘没加bpm锁。这就会出现这样一个情况:

                              -

                              一个page被进程A evict,进程A还没执行写回的时候这个page又被进程B捡回来了,因为还没写入所以磁盘空空如也。这时候pages_latch_这个细粒度锁不能防范这种情况,是因为此时这个page对应的container不是同一个,所以fid不同,细粒度锁不同导致寄。

                              -

                              解决方法是要么写的时候持有bpm锁,但是这太太慢了。另一个就是干脆直接在unpin的时候不带bpm锁顺便写回了。也即把写回从evict后移到unpin中立即写回:

                              -
                              if (pages_[fid].GetPinCount() == 0 && pages_[fid].IsDirty()) {
                              pages_[fid].is_dirty_ = false;
                              disk_manager_->WritePage(pages_[fid].GetPageId(), pages_[fid].GetData());
                              }
                              +
                              聚合函数

                              将一列数据作为整体,纵向计算

                              +

                              注意,聚合函数的计算会排除NULL值。如果不想让空置排除,可以尝试该方法:

                              +
                              select count(ifnull(math,0)) from students;
                              -

                              火焰图性能分析

                              -

                              FlameGraph

                              -

                              参考博客

                              -
                              -

                              看起来感觉大多性能损耗还是在bpm上,特别是LRU-K。也许是我的全局锁太暴力了。

                              -

                              out

                              -]]> - - - Lab0 - /2023/02/25/cs144$lab0/ - Lab0
                              -

                              本次实验一直在强调的一点就是,TCP的功能是将底层的零散数据包,拼接成一个reliable in-order的byte stream。这个对我来说非常“振聋发聩”(夸张了233),以前只是背诵地知道TCP的可靠性,这次我算是第一次知道了所谓“可靠”究竟可靠在哪:一是保证了序列有序性,二是保证了数据不丢失(从软件层面)。

                              -

                              还有一个就是大致了解了cs144的主题:实现TCP协议。也就是说,运输层下面的那些层是不用管的吗?不过这样也挺恰好,我正好在学校的实验做过对下面这些层的实现了,就差一个TCP23333这样一来,我的协议栈就可以完整了。

                              -
                              -
                              -

                              本次实验与TCP的关系:

                              -

                              在我们的webget实现中,正是由于TCP的可靠传输,才能使我们的http request正确地被服务器接收,才能使服务器的response正确地被我们接收打印。

                              -

                              而在ByteStream中,我们也做了跟TCP类似的工作:接收substring,并且将它们拼接为in-order的byte stream【由于在内存中/单线程,所以这个工作看起来非常简单】:

                              -
                              while(is_input_end == false&&pointer<length){
                              if(buffer.size() == capacity) break;
                              buffer.push_back(data[pointer]);
                              pointer++;
                              }
                              -
                              -

                              Fetch a Web page

                              主要是介绍了telnet指令

                              -

                              屏幕截图 2023-02-23 194758

                              -

                              Send yourself an email

                              用的是telnet带smtp参

                              -

                              Listening and connecting

                              上面的telnet是一个client program。接下来我们要把自己放在server的位置上。

                              -

                              用的是netcat指令。

                              -

                              image-20230223202202509

                              -

                              Use socket to write webget

                              这个确实不难,就是这个地方有点坑:

                              -
                              -

                              Please note that in HTTP, each line must be ended with “\r\n” (it’s not sufficient to use just “\n” or endl).

                              -
                              -

                              导致我跟400 Bad Request大眼瞪小眼了好久。。。

                              -
                              void get_URL(const string &host, const string &path) {
                              TCPSocket sock;
                              string tmp;
                              // sock.set_blocking(true);// 默认情况下即为true
                              sock.connect(Address(host,"http"));
                              sock.write("GET " + path + " HTTP/1.1\r\nHost: " +
                              host + "\r\nConnection: close\r\n\r\n");

                              while((tmp = sock.read(1)) != ""){
                              cout << tmp;
                              }
                              /*
                              上面那个写法不大规范,更规范的写法:
                              while(!sock.eof()){
                              cout << sock.read(1);
                              }
                              */
                              sock.close();
                              }
                              +
                                +
                              1. count 计算个数

                                +
                                select count(name) from students;# 有多少条记录
                                -

                                还有一点值得注意的是,当我这样时:

                                -
                                TCPSocket sock;
                                sock.set_blocking(false);
                                sock.connect(Address(host,"http"));
                                +

                                一般如果要看有多少记录,可以用count(主键),因为主键不为空。

                                +
                              2. +
                              3. max、min

                                +
                              4. +
                              5. sum 求和

                                +
                              6. +
                              7. avg 平均值

                                +
                              8. +
                              +
                              分组查询

                              分组之后查询的字段只能是两种:① 分组字段 ② 聚合函数。因为分组了之后再查具有个人特色的东西就没意义了。【高版本的mysql如果查询别的字段会报错】

                              +
                              select sex,avg(math) from students group by sex;
                              + +

                              image-20221207212103151

                              +
                              对分组结果进行条件限制

                              还可以在分组前对条件限定,使用WHERE

                              +
                              select sex,avg(math) from students where math >= 70 group by sex;
                              + +

                              或者在分组后限定,使用HAVING

                              +
                              select sex,avg(math),count(id) from students group by sex having count(id)>2;
                              # 或
                              select sex,avg(math),count(id) total from students group by sex having total>2;
                              + +
                              WHERE和HAVING的区别
                                +
                              1. WHERE在分组前限定,不满足where则不参与分组;HAVING在分组后限定,不满足having则不会被查询出来。
                              2. +
                              3. WHERE条件里不能有聚合函数,HAVING可以。
                              4. +
                              +
                              分页查询

                              image-20221207213958848

                              +

                              这种就是分页查询。

                              +
                              limit 开始的索引,每页查询的条数;
                              + +

                              limit只能在mysql使用。

                              +

                              DCL 管理用户,授权操作

                              管理用户

                              查询用户

                              image-20221219223932789

                              +

                              用户表存放地点↑

                              +
                              USE mysql;
                              SELECT * FROM USER;
                              + +
                              创建用户

                              注意,以下出现的”用户名”@”主机名” IDENTIFIED BY “密码”,不能在@两侧加空格,否则报错。

                              +
                              CREATE USER "用户名"@"主机名" IDENTIFIED BY "密码";
                              + +
                              删除用户
                              DROP USER "用户名"@"主机名";
                              + +
                              修改密码
                              -- 使用mysql自带的密码加密函数PASSWORD
                              -- 1
                              UPDATE USER SET PASSWORD = PASSWORD("新密码") WHERE USER = "用户名";
                              -- 2
                              SET PASSWORD FOR "用户名"@"主机名" = PASSWORD("新密码");
                              + +

                              image-20221219225036633

                              +

                              授权操作

                              查询权限
                              SHOW GRANTS FOR "root"@"%";
                              + +
                              授予权限
                              grant 权限列表 on 数据库名.表名 to '用户名'@'主机名';
                              + +

                              image-20221219225715075

                              +
                              撤销权限
                              revoke 权限列表 on 数据库名.表名 from '用户名'@'主机名';
                              + +

                              约束

                              非空约束

                              添加非空约束

                              CREATE TABLE stu(
                              id INT,
                              name VARCHAR(20) NOT NULL
                              );
                              ALTER TABLE stu MODIFY name VARCHAR(20) NOT NULL;
                              + +

                              删除非空约束

                              如果要去掉该约束,可以这么做:

                              +
                              ALTER TABLE stu MODIFY name VARCHAR(20);
                              + +

                              由于我们没写“NOT NULL ”,所以非空约束就被去掉了。感觉这点的解释挺有意思的。

                              +

                              唯一约束

                              添加唯一约束

                              某列值不能重复

                              +
                              CREATE TABLE stu(
                              id INT,
                              phone_number VARCHAR(20) UNIQUE
                              );
                              ALTER TABLE stu MODIFY phone_number VARCHAR(20) UNIQUE;
                              + +

                              但是注意,唯一约束允许多个NULL存在

                              +

                              删除唯一约束

                              唯一约束的删除方法跟前面的非空约束就完全不一样了。

                              +
                              ALTER TABLE stu DROP INDEX phone_number;
                              -

                              会报错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 ++;
                              }
                              +

                              主键约束

                              一张表只能有一个主键。主键非空且唯一。

                              +

                              添加主键约束

                              CREATE TABLE stu(
                              id INT PRIMARY KEY,
                              name VARCHAR(20)
                              );
                              ALTER TABLE stu MODIFY id INT PRIMARY KEY;
                              -

                              结果就是测试用例Timeout。我找了很久都不知道错在了哪,最后求助了场外观众【罪过……这次实验太不独立了】,学着他把length改成了这样:

                              -
                              int length = min(data.length(),capacity-buffer.size());
                              +

                              删除主键约束

                              ALTER TABLE stu DROP PRIMARY KEY;
                              -

                              发现成了。

                              -

                              我去看了看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;
                              +

                              自动增长

                              这东西一般都跟主键结合使用。

                              +

                              若某一列是数值类型,可以使用auto_increment关键字来完成值的自动增长。

                              +
                              CREATE TABLE stu(
                              id INT PRIMARY KEY AUTO_INCREMENT,
                              NAME VARCHAR(20)
                              );
                              INSERT INTO stu VALUES(NULL,'111');# 设NULL或自己指派都行。
                              -

                              具体实现

                              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(); }
                              -]]>
                              -
                              - - CMU15445 - /2023/03/13/cmu15445/ - -

                              实验官网

                              -

                              代码

                              +

                              自动增长的数据只跟上一个记录有关系。

                              +

                              外键约束

                              引言

                              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`;
                              + +
                              +

                              此在书中称为“等值连接和非等值连接”。

                              -

                              Project0 C++ Primer

                              Project1 Buffer Pool

                              Project2 B+Tree Index

                              Project3 Query Execution

                              -

                              In this project, you will implement the components that allow BusTub to execute queries. You will create the operator executors that execute SQL queries and implement optimizer rules to transform query plans.

                              -

                              实现SQL查询的执行,并且实现语句优化。

                              +

                              显式内连接

                              语法: 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

                              -

                              Background

                              Bustub Framewor

                              image-20231227153858926

                              -

                              AST

                              介绍完了bustub的框架之后,它对通过语法树进行查询优化进行了详细的样例介绍。

                              -

                              首先温习一下什么是语法树(abstract syntax tree, AST ):

                              -

                              SQL语句

                              -
                              Select `title`
                              From Books, Borrowers, Loans
                              Where Books.LC_NO = Loans.LC_NO and Borrowers.CARD_NO = Loans.CARD_NO
                              +

                              左外连接

                              语法: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`;
                              -

                              其语法树表示+优化结果如下图所示:

                              -

                              image-20231227155236633

                              -

                              算法如下,其关键思路就是选择投影尽早做,能移多下去就移多下去

                              -

                              image-20231227155806019

                              -

                              而这里15445介绍的也是这样的语法树优化算法。

                              -

                              首先记录一下它这几个专有名词对应的操作:

                              +

                              右外连接

                              语法:select 字段列表 from 表1 right [outer] join 表2 on 条件;

                              +
                              SELECT 	* FROM dept t2 RIGHT JOIN emp t1 ON t1.`dept_id` = t2.`id`;
                              + +

                              子查询

                              查询嵌套

                              -
                                -
                              1. Projection:投影
                              2. -
                              3. Filter:选择
                              4. -
                              5. MockScan:对一个表进行的扫描操作
                              6. -
                              7. Aggregation:聚合函数
                              8. -
                              9. NestedLoopJoin:嵌套循环连接
                              10. -
                              +

                              子查询中不允许使用ORDER BY

                              +

                              在实际运用中,内连接比子查询的效率更高

                              -

                              再结合它给的几个语法树的例子:

                              -
                              SELECT * FROM __mock_table_1;

                              === PLANNER ===
                              Projection { exprs=[#0.0, #0.1] } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
                              MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
                              === OPTIMIZER ===
                              MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
                              +

                              不相关子查询

                              +

                              image-20221219160800762

                              +
                              +
                              子查询结果单行单列

                              可用于WHERE条件

                              +
                              -- 查询工资最高的员工信息
                              SELECT * FROM emp WHERE emp.salary = (SELECT MAX(salary) FROM emp);
                              -
                              SELECT colA, MAX(colB) FROM
                              (SELECT * FROM __mock_table_1, __mock_table_3 WHERE colA = colE) GROUP BY colA;

                              === OPTIMIZER ===
                              Agg { types=[max], aggregates=[#0.1], group_by=[#0.0] }
                              NestedLoopJoin { type=Inner, predicate=(#0.0=#1.0) }
                              MockScan { table=__mock_table_1 }
                              MockScan { table=__mock_table_3 }
                              +
                              子查询结果多行单列

                              可以作为条件用IN关键字

                              +
                              -- 查询'财务部'和'市场部'所有的员工信息
                              SELECT * FROM emp WHERE dept_id IN (SELECT id FROM dept WHERE name = "财务部" OR name = "市场部");
                              -

                              image-20231227160450894

                              -
                              SELECT * FROM __mock_table_1 WHERE colA > 1;

                              === OPTIMIZER ===
                              Filter { predicate=(#0.0>1) } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
                              MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
                              +
                              子查询结果多行多列

                              可以当做一个新表,可以转化为普通内连接

                              +
                              -- 查询员工入职日期是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;
                              -
                              values (1, 2, 'a'), (3, 4, 'b');

                              === PLANNER ===
                              Projection { exprs=[#0.0, #0.1, #0.2] } | (__values#0.0:INTEGER, __values#0.1:INTEGER, __values#0.2:VARCHAR)
                              Values { rows=2 } | (__values#0.0:INTEGER, __values#0.1:INTEGER, __values#0.2:VARCHAR)
                              === OPTIMIZER ===
                              Values { rows=2 } | (__values#0.0:INTEGER, __values#0.1:INTEGER, __values#0.2:VARCHAR)
                              - -

                              可以看到,它大概是用缩进来表示了AST的父子关系。

                              -

                              我们课上学习的语法树中每个table标志对应着一个MockScan;笛卡尔积+选择操作可以表示为一个NestedLoopJoin。

                              -

                              对于这些输出的意义,指导书也给了详细的解释:

                              -

                              ColumnValueExpression

                              -

                              也即类似exprs=[#0.0, #0.1]#0意为第一个子节点(不是第一个表的意思。。)

                              -

                              Project

                              -

                              In this project, you will add new operator executors and query optimizations to BusTub. BusTub uses the iterator (i.e., Volcano) query processing model, in which every executor implements a Next function to get the next tuple result. When the DBMS invokes an executor’s Next function, the executor returns either (1) a single tuple or (2) an indicator that there are no more tuples. With this approach, each executor implements a loop that continues calling Next on its children to retrieve tuples and process them one by one.

                              -
                              -]]> - - labs - - - - Project1 Buffer Pool - /2023/03/13/cmu15445$lab1/ - Project1 Buffer Pool

                              先放个通关记录~

                              -

                              image-20230330235300907

                              -
                              -

                              特别鸣谢:

                              -

                              某不愿透露姓名的友人hhj

                              -

                              大佬的实验过程

                              -

                              大佬的性能优化

                              +

                              相关子查询

                              +

                              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  "级别字符串";
                              +
                              -

                              During the semester, you will build a disk-oriented storage manager for the BusTub DBMS.

                              -

                              注:DBMS(Database Management System),比如说Oracle数据库

                              -

                              The first programming project is to implement a buffer pool in your storage manager.The buffer pool is responsible for moving physical pages back and forth from main memory to disk.也就是负责物理页从磁盘的读写

                              -

                              It allows a DBMS to support databases that are larger than the amount of memory available to the system. 是的,这其实就跟内存换入换出差不多。我们现在是不是要在用户态实现这个功能?我记得xv6似乎是没有这个机制的。有点小期待呀。不过这部分感觉说不定和xv6的磁盘管理(使用双向链表管理buffer cache),及其的改进版本(lab:locking 使用哈希+双向链表)比较类似。

                              -
                              -

                              注:xv6确实没有内存换入换出机制,其是固定大小的内存空间。但xv6的文件系统有采用LRU算法的buffer cache(怪不得有什么数据库型的文件系统,这两个确实有点像)。

                              -
                              -

                              The buffer pool’s operations are transparent to other parts in the system. For example, the system asks the buffer pool for a page using its unique identifier (page_id_t) and it does not know whether that page is already in memory or whether the system has to retrieve it from disk.

                              -

                              Your implementation will need to be thread-safe.

                              -
                              -

                              总结

                              由于这几天时间比较零散+事情比较多,因此完成的总时间数不一定值得参考:26h(乐)

                              -

                              本次实验要说简单也还算简单。大概就是实现一个database与磁盘交换页的buffer pool,机制类似于内存换入换出。而实现这个buffer pool,首先得实现换入换出算法,也即task1的LRU-K算法。再然后就是在我们的LRU-K算法的基础上,实现真正的buffer pool(真正指:真正地存储以及读写磁盘、向外暴露接口),也即BufferPoolManager。最后,我们会实现类似于lock_guard这样结构的PageGuard,用于自动释放页引用和读写锁。最后的最后,我们会对实现的buffer pool进行性能优化,优化方向包括细粒度化锁以实现并行IO、针对特定应用场景调整LRU-K策略等。

                              -

                              这四者都是相互联系相互递进的,我认为每一个task都设计得非常不错,写完了之后对它所涉及的知识点都有了更深刻的理解。我认为其中最优美的一点就是LRU-K算法与buffer pool的解耦,这个设计让我十分地赞叹。

                              -

                              最后,再对我的完成情况进行一个评价。本次实验确实内容不是很难【除了性能调优部分,这个我是真不懂QAQ】,毕竟它指导书以及代码注释都给了详细的步骤参考,我之所以做了那么久一是因为我有不好的习惯,就是没认真看指导书和提示就开始按着自己的理解写,然后写完就直接开始debug开始交了;二是因为这几天学业的破事太多、竞赛也逐步开始了,因而战线拉得太长,总耗时就太多了。

                              -

                              因而,吸取经验,我之后coding完了之后,再照着指导书仔仔细细地过一遍自己的代码。同时,15445这个实验我也决定先暂时搁置,毕竟接下来这两个月应该会在竞赛和学业两头转,实在不能抽出很大段时间继续写了。

                              -

                              就酱。

                              -

                              Task1 LRU-K

                              -

                              This component is responsible for tracking page usage in the buffer pool.

                              -

                              The LRU-K algorithm evicts a frame whose backward k-distance is maximum of all frames in the replacer. LRU-K 算法驱逐一个帧,其backward k-distance是替换器中所有帧的最大值。

                              -

                              Backward k-distance is computed as the difference in time between current timestamp and the timestamp of kth previous access. backward k-distance=现在的时间戳 - 之前第k次访问时的时间戳

                              -

                              A frame with fewer than k historical accesses is given +inf as its backward k-distance. 不足k次访问的帧的backward k-distance应该设置为inf(对应上图左边那个访问记录队列吧)

                              -

                              **When multiple frames have +inf backward k-distance, the replacer evicts the frame with the earliest overall timestamp (i.e., the frame whose least-recent recorded access is the overall least recent access, overall, out of all frames).**如果有多个inf的结点,按照LRU规则淘汰(也即上图左边那个历史记录队列采取LRU规则)

                              -

                              The maximum size for the LRUKReplacer is the same as the size of the buffer pool since it contains placeholders for all of the frames in the BufferPoolManager. However, at any given moment, not all the frames in the replacer are considered to be evictable. The size of LRUKReplacer is represented by the number of evictable frames. The LRUKReplacer is initialized to have no frames in it. Then, only when a frame is marked as evictable, replacer’s size will increase. size为可驱逐的frame数而非所有frame数。

                              +

                              通过之后老师说的内容,感觉有了点个人的感悟:

                              +

                              级别1寻找数据可能优先从缓冲区找;级别2相当于不能读到缓冲区内容;级别3可能相当于在开启事务前对表做了个快照?级别4应该就是直接上了把互斥锁,同一时刻只能一个事务读写。

                              -

                              正确思路

                              本次实验要我们实现的是一个LRU-K算法的页面置换器。

                              -

                              LRU-K算法是对LRU算法和LFU算法的折中优化,平衡了LFU和LRU的性能和开销的同时,也解决了缓存污染问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。具体来说,它维护了一个backward k-distance,其计算方法:

                              -
                                -
                              1. 如果已经被访问过k次: backward k-distance = current_timestamp_ - 倒数第k次访问的时间戳
                              2. -
                              3. 如果还没被访问过k次: backward k-distance = +inf
                              4. -
                              -

                              页面驱逐规则:

                              -
                                -
                              1. 驱逐 backward k-distance 最大的页。

                                -

                                也即情况2总是优先会比情况1被驱逐;每次优先驱逐previous k次访问最早的页面。

                                +

                                JDBC

                                概念

                                Java Database Connectivity Java语言操作数据库

                                +

                                image-20221220141025613

                                +

                                image-20221220141214259

                                +

                                快速入门

                                  +
                                1. 导入驱动jar包

                                  +

                                  ① 新建libs目录

                                  +

                                  ② 把jar包复制到libs目录下

                                  +

                                  ③ 右键libs目录 add as library

                                2. -
                                3. 当有多个页值为+inf,则采取FIFO规则进行驱逐。

                                  +
                                4. 注册驱动

                                5. -
                                -

                                故而,在具体实现中,为了便于管理,我将此拆分为两个队列:

                                -
                                -

                                思路来自:LRU . LFU 和 LRU-K 的解释与区别

                                -

                                image-20230323205851168

                                -
                                  -
                                1. 数据第一次被访问,加入到访问历史列表;

                                  +
                                2. 获取数据库连接对象 Connection

                                3. -
                                4. 如果数据在访问历史列表里后没有达到K次访问,则按照一定规则(FIFO,LRU)淘汰;

                                  +
                                5. 定义sql语句

                                6. -
                                7. 当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列删除,将数据移到缓存队列中,并缓存此数据,缓存队列重新按照时间排序;

                                  +
                                8. 获取执行sql语句的对象 Statement

                                9. -
                                10. 缓存数据队列中被再次访问后,重新排序;

                                  +
                                11. 执行sql,接收返回的结果

                                12. -
                                13. 需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰“倒数第K次访问离现在最久”的数据。

                                  +
                                14. 处理结果

                                  +
                                15. +
                                16. 释放资源

                                -
                                -

                                每个页面结构持有一个时间戳队列即可:

                                -

                                image-20231128152139485

                                -

                                感想

                                刚coding完

                                task1的内容就是实现对一堆frame_id的LRU-K算法管理,挺简单的(也可能是测试用例少我错误没排查出来2333)

                                -

                                我并没有用默认给的模板的unorder_map,也没有用默认给的模板思路(但原理以及最终效果是差不多的,就是没用它的方法),而是选择类似像上面这张图一样,分成两个队列实现,一个队列visit_record_存储那些访问次数<k的数据,另一个队列cache_data_存储那些访问次数>=k的顺序,每次优先淘汰visit_record_中的数据,两个队列都采用LRU的方式管理。与此同时,我觉得LRU管理时间戳只用记录最新访问的就行,所以将历史访问时间戳队列改成了只有一个变量。

                                -

                                终于通过online-test

                                -

                                参考:

                                -

                                FIFO和LRU这里面的实例非常直观地说明了两种算法的差异,可以跟着手推感受一下

                                -

                                pro1这个用的是我上面的那个想法,是错的。但是评论很值得参考:

                                -

                                image-20230329230045194

                                -

                                pro1这个评论的“偷测试用例”xswl,虽然这次没用,但以后说不定能用上:

                                -

                                image-20230329230139300

                                -
                                -
                                正确思路

                                ……简单个屁!!

                                -

                                算法上,上面错误的算法确实很简单;而正确的算法也确实很简单。那么难的是什么呢?我觉得难的还是搞清楚它要我们实现的究竟是上面东西。

                                -

                                结合指导书这段话:

                                -
                                -

                                The LRU-K algorithm evicts a frame whose backward k-distance is maximum of all frames in the replacer. 每次驱逐 backward k-distance最大的

                                -

                                那么 backward k-distance是什么?

                                -

                                Backward k-distance is computed as the difference in time between current timestamp and the timestamp of kth previous access. backward k-distance = current_timestamp_ - 倒数第k次访问的时间戳

                                -

                                A frame with fewer than k historical accesses is given +inf as its backward k-distance. 没有达到k次访问的, backward k-distance为+inf。也就是说,每次优先从历史访问队列清除元素。

                                -

                                【**When multiple frames have +inf backward k-distance, the replacer evicts the frame with the earliest overall timestamp (i.e., the frame whose least-recent recorded access is the overall least recent access, overall, out of all frames).**】当历史访问队列有多个元素,就驱逐英文描述那样的frame。

                                -
                                -

                                我们可以发现,它这个对于两个队列的LRU,并非我们原来算法那样,对于每个frame,针对其最新的访问时间戳,也即history_.back(),进行LRU淘汰;而是,针对其倒数第k新的访问记录,也即history_.front() && history_.size()<=k,进行LRU淘汰。

                                -

                                其中,由于历史访问队列的记录少于k个,因而其事实上从k-distance算法退化为了FIFO算法。【感受一下这一点的优美:FIFO实际上是k-distance的特例】

                                -

                                我们上面的算法比较的是history_.back(),所以可以省略时间戳队列为一个变量,然后将两个队列使用FILO的形式组织起来。正确算法就不能这么简单了,要按front排序的话,实现开销可能更大,所以下面就采用了map形式来实现logn的查找。

                                -
                                关于LRU的翻译

                                这里一个点我其实还是很疑惑的,完全想不通。

                                -

                                就是,对缓存队列实现k-distance算法没毛病,这段话已经写得很清楚了。

                                -
                                -

                                Backward k-distance is computed as the difference in time between current timestamp and the timestamp of kth previous access. backward k-distance = current_timestamp_ - 倒数第k次访问的时间戳

                                -
                                -

                                但是,为什么历史访问队列要用FIFO呢?是我英语不好吗,这段话不是实现纯正LRU的意思吗:

                                -
                                -

                                【**When multiple frames have +inf backward k-distance, the replacer evicts the frame with the earliest overall timestamp (i.e., the frame whose least-recent recorded access is the overall least recent access, overall, out of all frames).**】当历史访问队列有多个元素,就驱逐英文描述那样的frame。

                                -
                                -

                                我翻译一下:

                                -

                                当多个frame有+inf这个 backward k-distance的时候,replacer需要驱逐拥有全部(overall)frame中最早的timestamp的frame。(也就是说,frame,它的最近访问记录是所有frame里面最早的)

                                -

                                这样确实看起来就是要用LRU。

                                -

                                但其实,是我英语不好。咨询了场外热心人士hhj之后,我才修订出了如下版本:

                                -

                                当多个frame有+inf这个 backward k-distance的时候,replacer需要驱逐拥有全部(overall)frame中最早的timestamp的frame。(也就是说选择一个frame,这个frame的最不近的访问记录,是所有frame中最近最少访问的)【也即这个frame的history的front是所有frame中最早的,也即使用FIFO算法】

                                -

                                可见,正确解法确实是没问题的,就是理解上很困难。要是可以配个实例就好了QAQ

                                -

                                所以说,所谓LRU(Least Recently Used)的直译还是最不近使用,也即最近最少使用。里面这个least不是用来修饰recent表示recent程度深的,相反它表示的是recent的程度浅。英语不好的惨痛教训啊。

                                -

                                image-20230330160207757

                                -

                                最后一下子交了这么多次才过。绷不住了。

                                -

                                Task2 BufferPoolManager

                                -

                                The BufferPoolManager is responsible for fetching database pages from the DiskManager and storing them in memory.从DiskManager中取出页,然后存入内存。

                                -

                                也就是说,我们的Buffer Pool是磁盘到内存的映射,我们在Task1实现了内存部分的管理数据结构?

                                -

                                The BufferPoolManager can also write dirty pages out to disk when it is either explicitly instructed to do so or when it needs to evict a page to make space for a new page.也要负责dirty页的写回

                                -

                                You will also not need to implement the code that actually reads and writes data to disk (this is called the DiskManager in our implementation). We will provide that functionality. DiskManager已给出

                                -

                                All in-memory pages in the system are represented by Page objects. Each Page object contains a block of memory that the DiskManager will use as a location to copy the contents of a physical page that it reads from disk. Page 是可复用的内存页容器

                                -

                                The Page object’s identifer (page_id) keeps track of what physical page it contains; if a Page object does not contain a physical page, then its page_id must be set to INVALID_PAGE_ID.

                                -

                                也就是说,page_id表示的是实际的物理页号;frame_id表示的是你的Page容器的序号,同时也是LRU的对象。你需要一个类似<fid, pid>这样的map来记录这二者的映射。具体是通过:

                                -
                                /** Array of buffer pool pages. */
                                Page *pages_; // <fid, pid>
                                /** Page table for keeping track of buffer pool pages. */
                                std::unordered_map<page_id_t, frame_id_t> page_table_; // <pid, fid>
                                +
                                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();
                                }
                                }
                                -

                                Each Page object also maintains a counter for the number of threads that have “pinned” that page. Your BufferPoolManager is not allowed to free a Page that is pinned. 有引用计数机制

                                -

                                Each Page object also keeps track of whether it is dirty or not. It is your job to record whether a page was modified before it is unpinned. Your BufferPoolManager must write the contents of a dirty Page back to disk before that object can be reused.需要track dirty,并且这是你要干的;要写回,这也是你要干的

                                -

                                Your BufferPoolManager implementation will use the LRUKReplacer class that you created in the previous steps of this assignment. The LRUKReplacer will keep track of when Page objects are accessed so that it can decide which one to evict when it must free a frame to make room for copying a new physical page from disk. When mapping page_id to frame_id in the BufferPoolManager, again be warned that STL containers are not thread-safe.

                                -
                                -

                                感想

                                刚coding完

                                task2说得比较复杂,实现的函数较多,实际coding细节也比较繁琐,但debug倒是很轻松。

                                -

                                主要内容就是实现BufferPoolManager,在task1实现的LRU-K算法的基础上,写具体的内存换入换出的接口逻辑。

                                -

                                再次回顾我们整个project1的目的:实现一个从磁盘到内存的buffer。task1只是实现了一个内存页换入换出的LRU-K算法部分,task2则基于算法部分,实现了具体与上层交互的像样的逻辑。

                                -

                                我认为这其中一个亮点就是,它非常完美地将LRU-K算法和具体的上层逻辑进行了解耦。LRU-K只需关注如何将这一堆freme_id组织起来组织好,而无需关心具体内存页存放在哪,以及对应frame淘汰之后内存页又何去何从,因为这些逻辑都会由上层实现;而上层逻辑也无需关心具体的淘汰页算法【LRU-K/LRU/LFU,只需替换replacer_就可以替换换入换出策略】,而只需打好evictable标记,并且在调用evict方法之后做好后处理(如内存释放等等等)即可。

                                -

                                这其中有一个小细节也值得借鉴,即从page_id_frame_id_的转化。frame_id_有界,比较方便LRU-K算法实现,并且进行了LRU-K算法的容量控制,同时由于算法和上层逻辑的容量相同,故而也是pages_的索引号;而page_id_不能有界,因为实际上访问到的物理页不可能只共享pool_size_个序列号。故而在这样解耦实现的基础上,二者缺一不可。

                                -

                                还有frame_id_的复用,它是采用了类似我们日常生活中取号那样,要用号时从队列头取,不用号时塞回队列尾就行,这种方式我觉得还挺有意思。

                                -

                                其他部分虽然步骤繁杂,但理解难度不高,而且它提示得也很保姆了,所以不多bb。

                                -

                                通过online-test

                                确实算简单了,我主要倒在没有认真看它的需求,这应该是语文问题(绷

                                -

                                一个是FetchPage这里:

                                -

                                image-20230330162812680

                                -

                                如果所求物理页存在于buffer pool,直接返回+record access即可,不用再写回+读入。因为它的提示这边:

                                -

                                image-20230330162941774

                                -

                                这个是句号。也就是说后面那些写回啊read啊,是没找到时才做的,不是并列关系。

                                -

                                这也很合理,毕竟你找到所需页就说明不用从磁盘读入,也即找到所需页=直接返回即可。

                                -

                                另一个是UnpinPage这里:

                                -

                                image-20230330163031739

                                -

                                不应该写is_dirty_ = is_dirty,因为它的提示这边:

                                -

                                image-20230330163058921

                                -

                                可见参数is_dirty为true是需要设置为dirty,为false的话没有别的意义,保持原来值就行。

                                -

                                还有一个就是,在Page类中声明了friend:

                                -

                                image-20230330163337929

                                -

                                故而BufferPoolManager可以直接访问Page的私有成员变量,而无需手动为Page添加Getter/Setter方法。

                                -

                                Task3 Page Guard

                                这是要写我们在上面用的那个PageGuard?这让我想起了Lab0的ValueGuard

                                -
                                template <class T>
                                class ValueGuard {
                                public:
                                ValueGuard(Trie root, const T &value) : root_(std::move(root)), value_(value) {}
                                auto operator*() const -> const T & { return value_; }

                                private:
                                Trie root_;
                                const T &value_;
                                };
                                +

                                优化版【增加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);
                                }
                                }
                                }
                                }
                                }
                                -

                                不过其实这两个是不一样的。本次要实现的Page Guard的语义更类似lock_guard

                                +

                                详解各个类

                                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类干的。这个静态块仅仅用于简化代码书写。

                                -

                                我们需要手动调用UnpinPage,但这中就跟new/delete、malloc/free一样都要靠人脑来记住,不大安全。

                                -

                                You will implement BasicPageGuard which store the pointers to BufferPoolManager and Page objects. A page guard ensures that UnpinPage is called on the corresponding Page object as soon as it goes out of scope. 【也许这需要在析构函数中实现?】Note that it should still expose a method for a programmer to manually unpin the page.仍然需要提供UnPin方法。

                                -

                                As BasicPageGuard hides the underlying Page pointer, it can also provide read-only/write data APIs that provide compile-time checks to ensure that the is_dirty flag is set correctly for each use case.这个思想很值得学习。

                                -

                                In the future projects, multiple threads will be reading and writing from the same pages, thus reader-writer latches are required to ensure the correctness of the data. Note that in the Page class, there are relevant latching methods for this purpose. Similar to unpinning of a page, a programmer can forget to unlatch a page after use. To mitigate the problem, you will implement ReadPageGuard and WritePageGuard which automatically unlatch the pages as soon as they go out of scope.

                                +

                                注意:mysql5之后的版本,这一步注册驱动可以省略。

                                +

                                image-20221220151045275

                                +

                                配置文件里自动帮你注册了。我想原理应该是让本文件的类自动加载。

                                -

                                感想

                                怎么说,其实只用仔细看相关文档和它的要求就不难,但你懂的我的尿性就是不细看文档,所以这里我也用gdb调了蛮久才过的。正确思路没什么好说的,直接记录下我觉得比较有意义的错误吧。

                                -

                                错误集锦

                                析构函数的调用

                                image-20230330233252372

                                -

                                在这个用例中,退出“}”会调用两次析构函数。

                                -
                                奇怪的死锁
                                debug过程

                                我在coding的过程中,遇到了一个很神奇的死锁现象。

                                -

                                在这里page->WLatch();这句会死锁,而且还是在第一次调用FetchWritePage()时死锁的:

                                -
                                WritePageGuard(BufferPoolManager *bpm, Page *page) : guard_(bpm, page) {
                                page->WLatch();
                                }
                                +
                                获取数据库连接
                                Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/helloworld","root","root");
                                -

                                但是添加了一句page->WUnlatch();

                                -
                                WritePageGuard(BufferPoolManager *bpm, Page *page) : guard_(bpm, page) {
                                page->WUnlatch();
                                page->WLatch();
                                }
                                +

                                url的语法:”jdbc:mysql://IP地址:端口号/数据库名称”

                                +

                                image-20221220151425953

                                +

                                Connection

                                数据库连接对象。

                                +
                                获取Statement对象
                                Statement createStatement() throws SQLException;
                                PreparedStatement prepareStatement(String sql) throws SQLException;
                                -

                                它就不会死锁了。

                                -

                                这很奇怪,到底是发生了什么?我用GDB调了半天,在RWLatch.WLock()处打了断点,也没发现在这之前有调用过lock()。于是我就去看了下std::shared_mutex的官方文档(当然,这中间想了很久也不知道怎么办):

                                -

                                image-20230331222601644

                                -

                                我就怀疑是不是我哪里写错了,所以就干了这种undefined的事,然后就导致死锁了。于是我写了个测试程序:

                                -

                                image-20230330195418370

                                -

                                发现,当在调用WLock(也即std::shared_mutex::lock())之前,如果多调了一次XUnlock(也即std::shared_mutex::unlock()或者std::shared_mutex::unlock_shared()),就会卡住。

                                -

                                这说明确实发生了不匹配问题。于是我就在Page中添加了两个成员变量用来记录上锁和解锁的次数,并且在gurad test中打印了出来,结果发现:

                                -

                                image-20230330233018049

                                -

                                确实发生了不匹配问题,是在这里:

                                -

                                image-20230330233252372

                                -

                                之后用gdb调下就发现错误了,不赘述了。

                                -
                                另外的想法

                                在出现死锁问题时,我是想着,会不会是测试程序中,对同一页获取了一次ReadGuardPage对象之后,再对同一页获取Read/WriteGuardPage导致的呢?于是我就开始思考如何防范这个流程,最后写下了这样的代码:

                                -
                                auto BufferPoolManager::FetchPageRead(page_id_t page_id) -> ReadPageGuard {
                                Page *page = FetchPage(page_id);
                                bool should_release = true;
                                if (!page->rwlatch_.try_lock_shared()) {
                                // 说明此时已有read/write锁
                                should_release = false;
                                }
                                return {this, pagei, should_release};
                                }

                                auto BufferPoolManager::FetchPageWrite(page_id_t page_id) -> WritePageGuard {
                                Page *page = FetchPage(page_id);
                                bool should_release = true;
                                if (!page->rwlatch_.try_lock()) {
                                // 获取write锁失败,可能原因:该进程持有write锁、别的进程有read锁、该进程持有read锁
                                if (page->rwlatch_.try_lock_shared()) {
                                // 成功read,说明是别的进程有read锁
                                page->rwlatch_.unlock_shared();
                                // 等待
                                page->rwlatch_.lock();
                                } else {
                                // 说明当前进程有read/write锁
                                should_release = false;
                                }
                                }
                                return {this, pagei, should_release};
                                }
                                +
                                管理事务
                                开启事务
                                void setAutoCommit(boolean autoCommit) throws SQLException;
                                -

                                但很遗憾的是,我发现是无法区分当前进程持有write还是read锁的。也许有别的办法但我没想起来。

                                -

                                总之,我认为这段代码还是很有参考价值的,姑且放着先。

                                -

                                Task4 性能调优

                                -

                                参考:

                                -

                                CMU 15-445 2023 P1 优化攻略 [rank#3] 写得非常细致,思路很清晰

                                -

                                CMU 15-445 Project 1 (Spring 2023) 优化记录

                                -
                                -
                                -

                                我的实现有一些并发小问题,详见lab2的并发部分~

                                +

                                设置参数为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

                                -

                                lru-k的算法优化是自己想的,并行IO的优化思路全部来自 CMU 15-445 Project 1 (Spring 2023) 优化记录,我只是把这位大佬的思路自己实现了一遍。感觉还是太菜了,面对这种实际场景毫无还手之力一点思路没有QAQ但正是如此,这个细粒度化锁的小task才值得学习。

                                -

                                放上优化前后性能对比:

                                -

                                image-20230331000020775

                                -

                                image-20230404140838247

                                -

                                Better replacer algorithm

                                -

                                In the leaderboard test, we will have multiple threads accessing the pages on the disk. There are two types of threads running in the benchmark:在具体的benchtest中,可以分为两类线程。

                                +
                                执行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. Scan threads. Each scan thread will update all pages on the disk sequentially. There will be 8 scan threads.
                                2. -
                                3. Get threads. Each get thread will randomly select a page for access using the zipfian distribution. There will be 8 get threads.
                                4. +
                                5. 这东西也得Close

                                  +
                                6. +
                                7. 如果想做“查询到了结果则返回true”这样的操作,不应该使用这样的代码:

                                  +
                                  if (resultSet != null)  return true;
                                  else return false;
                                  + +

                                  而应该这样:

                                  +
                                  return resultSet.next();
                                -

                                Given that get workload is skewed(有偏向性的)(i.e., some pages are more frequently accessed than others), you can design your LRU-k replacer to take page access type into consideration, so as to reduce page miss.

                                -
                                -

                                解决方法

                                我们可以回想起当初选择LRU-K而不选择LRU算法的原因:缓存污染。

                                -
                                -

                                LRU 一种缓存淘汰算法

                                -

                                缓存污染:

                                -

                                LRU因为只需要一次访问就能成为最新鲜的数据,当出现很多偶发数据时,这些偶发的数据也会被当作最新鲜的,从而成为缓存。但其实这些偶发数据以后并不会是被经常访问的。

                                -
                                -

                                而在这里也是同理。我们的benchtest中,scan线程是顺序地访问磁盘上所有页,而get线程是遵从zip分布地访问,显然get线程的access记录比scan线程的有价值的多,并且scan线程的数据是很容易污染get线程的。

                                -

                                所以,我的解决方法是,如果某个页被第一次访问,且该访问方式为SCAN,则RecordAccess进入历史访问队列;如果某个页不是被第一次访问,且访问方式为SCAN,则不做任何处理。不用修改UnpinPage的处理方式。

                                -

                                Parallel I/O operations

                                -

                                Instead of holding a global lock when accessing the disk manager【不要在访问disk_manager_的时候使用bpm的全局锁latch_】, you can issue multiple requests to the disk manager at the same time. This optimization will be very useful in modern storage devices, where concurrent access to the disk can make better use of the disk bandwidth.

                                -
                                -

                                解决方法

                                详细的解决方法大佬这边已经说得很清楚了,接下来我就对其总体的做法进行一点总结,加上一些个人理解。

                                -

                                我刚看到这个需求的时候是这么做的:

                                -
                                if (pages_[fid].IsDirty()) {
                                latch_.unlock();
                                disk_manager_->WritePage(pages_[fid].GetPageId(), pages_[fid].GetData());
                                latch_.lock();
                                }
                                +

                                PreparedStatement

                                简介
                                //An object that represents a precompiled SQL statement.
                                //表示预编译的sql语句的对象
                                public interface PreparedStatement extends Statement
                                -

                                也即在原来代码的基础上做简单的改动,每次执行到涉及磁盘读写的地方,就暂时地开一下锁。但其实这样是不行的,当多个线程访问bpm,线程A在这里开锁执行Write,线程B正好得到锁,然后对pages_[fid]执行比如说ResetMemory操作,这样就寄了。

                                -

                                所以,在磁盘读写的时候,我们仍然需要使用锁保护,只不过我们需要选择粒度更细的锁。这时我们就可以想到在page_guard里常用的page自带的锁。在这里用page锁,既能够锁保护,又符合语义,看起来非常完美:

                                -
                                pages_[fid].WLatch();
                                latch_.unlock();
                                if (pages_[fid].IsDirty()) {
                                disk_manager_->WritePage(pages_[fid].GetPageId(), pages_[fid].GetData());
                                }
                                latch_.lock();
                                pages_[fid].WUnlatch();
                                +

                                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);
                                }
                                }
                                }
                                -

                                但由于我们在returnpage_guard的时候会获取锁,因而在这样的情况下,会发生死锁:

                                -
                                auto reader_guard_1 = bpm->FetchPageRead(page_id_temp);
                                auto reader_guard_2 = bpm->FetchPageRead(page_id_temp);
                                +
                                用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);
                                }
                                }
                                }
                                }
                                -
                                -

                                在这里我们首先获取reader_guard_1 ,持有了该 page 的读锁,并允许其他线程读;但在获取reader_guard_2时,FetchPage会在释放 bpm 写锁前,请求该 page 的写锁;但由于reader_guard_1已经申请了该 page 的读锁,就会造成死锁,与预期结果不符。

                                -
                                -

                                因而,我们就可以选择在bpm内部,单独为pages_数组的每一页都维护一个锁,在每个对page页属性进行读写的地方进行锁定:

                                -
                                std::shared_mutex latch_;
                                std::vector<std::mutex> pages_latch_;
                                +

                                使用

                                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;
                                }
                                -

                                然后对代码进行重排序,尽量分离bpm内部成员和page内部成员属性的修改:(以FetchPage为例)

                                -
                                auto BufferPoolManager::FetchPage(page_id_t page_id, [[maybe_unused]] AccessType access_type) -> Page * {
                                ...
                                if (free_list_.empty()) {
                                frame_id_t fid;
                                if (!replacer_->Evict(&fid)) { ... }
                                // 这些地方不涉及对page的读写,只涉及对bpm内部成员的读写
                                page_table_.erase(page_table_.find(pages_[fid].GetPageId()));
                                page_table_.insert(std::make_pair(page_id, fid));

                                replacer_->RecordAccess(fid, access_type);
                                replacer_->SetEvictable(fid, false);

                                // 两个锁的交接点
                                pages_latch_[fid].lock();
                                latch_.unlock();

                                if (pages_[fid].IsDirty()) {
                                disk_manager_->WritePage(pages_[fid].GetPageId(), pages_[fid].GetData());
                                }

                                Page *res = &(pages_[fid]);
                                res->page_id_ = page_id;
                                res->ResetMemory();
                                disk_manager_->ReadPage(page_id, res->GetData());
                                res->is_dirty_ = false;

                                res->pin_count_ = 1;

                                pages_latch_[fid].unlock();
                                return res;
                                }
                                // 这些地方不涉及对page的读写,只涉及对bpm内部成员的读写
                                frame_id_t fid = free_list_.front();
                                free_list_.pop_front();
                                page_table_.insert(std::make_pair(page_id, fid));

                                replacer_->RecordAccess(fid, access_type);
                                replacer_->SetEvictable(fid, false);

                                // 两个锁的交接点
                                pages_latch_[fid].lock();
                                latch_.unlock();

                                Page *res = &(pages_[fid]);
                                res->page_id_ = page_id;
                                res->ResetMemory();
                                disk_manager_->ReadPage(page_id, res->GetData());
                                res->is_dirty_ = false;

                                res->pin_count_ = 1;
                                pages_latch_[fid].unlock();

                                return res;
                                }
                                +

                                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);
                                }
                                }
                                }
                                -

                                其他地方也是一样。就不多赘述了。

                                -
                                一个小地方

                                当外界需要对页进行读写时,需要使用page自带的锁;而当bpm内部需要对页进行读写时,则使用的是bpm内部自带的页锁。

                                -

                                这句话说完,相信危险性已经显而易见了:我们使用了两把不同的锁维护了同一个变量!而且可能会有两个线程分别持有这两个锁,对这个变量并发更新!

                                -

                                但其实,在当前这个场景,这么做是没问题的。

                                -

                                外界实质上只能对page的data字段进行读写。因而,有上述危险的,实质上就只有bpm中会对data字段进行改变的地方,也即bpm::NewPage()bpm::FetchPage()bpm::DeletePage()这三个地方。

                                -

                                而在前两个地方,我们会使用到的page都是闲置/已经被释放的页,因而外界不可能,也即不可能有别的线程,会持有page的锁并且对其修改;同样的,在第三个地方,我们会使用的page也是pincount==0的页,仅有当前线程在对其进行读写。

                                -

                                因而,综上所述,这样做是并发安全的。

                                -]]> - - - 编译原理 - /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

                                +

                                数据库连接池

                                其实就是上面的JDBC中的Connection的对象池。

                                +

                                image-20221222212904151

                                +

                                C3P0

                                基本使用

                                非常简单,就是改一下Connection的获取,写一下xml就行。

                                +
                                设置配置文件

                                固定放在src目录下。名字必须为c3p0-config.xml或者c3p0.properties

                                +
                                <c3p0-config>
                                <!-- 使用默认的配置读取连接池对象 -->
                                <default-config>
                                <!-- 连接参数 -->
                                <property name="driverClass">com.mysql.jdbc.Driver</property>
                                <property name="jdbcUrl">jdbc:mysql://localhost:3306/helloworld</property>
                                <property name="user">root</property>
                                <property name="password">root</property>

                                <!-- 连接池参数 -->
                                <property name="initialPoolSize">5</property>
                                <property name="maxPoolSize">10</property>
                                <!-- 如果超过此超时时间,就说明数据库连接失败 -->
                                <property name="checkoutTimeout">3000</property>
                                </default-config>

                                <named-config name="otherc3p0">
                                <!-- 连接参数 -->
                                <property name="driverClass">com.mysql.jdbc.Driver</property>
                                <property name="jdbcUrl">jdbc:mysql://localhost:3306/day25</property>
                                <property name="user">root</property>
                                <property name="password">root</property>

                                <!-- 连接池参数 -->
                                <property name="initialPoolSize">5</property>
                                <property name="maxPoolSize">8</property>
                                <property name="checkoutTimeout">1000</property>
                                </named-config>
                                </c3p0-config>
                                + +

                                可以注意到,xml文件里面可以保存多套配置,比如上面的示例代码就保存了两套配置,default-config和name=”otherc3p0”的config。

                                +

                                ComboPooledDataSource有一个含参构造器:

                                +
                                public ComboPooledDataSource(String configName) {
                                super(configName);
                                }
                                + +

                                就可以传入config的名称指定要用的配置信息。

                                +
                                使用
                                DataSource cpds = new ComboPooledDataSource();
                                Connection conn = null;
                                try{
                                conn = cpds.getConnection();
                                //正常使用......
                                } catch(Exception e){

                                } finally{
                                if (conn != null){
                                try {
                                //正常使用关闭方法
                                conn.close();
                                } catch (SQLException e) {
                                throw new RuntimeException(e);
                                }
                                }
                                }
                                + +

                                Druid

                                基本使用

                                设置配置文件

                                Druid的配置文件可以放在任意路径下,随便取名字。因为到时候需要指定配置文件。使用的是Properties文件。

                                +
                                driverClassName=com.mysql.jdbc.Driver
                                url=jdbc:mysql://localhost:3306/helloworld
                                username=root
                                password=root
                                initialSize=5
                                maxActive=10
                                maxWait=3000
                                + +
                                使用
                                //导入配置文件
                                Properties pro = new Properties();
                                pro.load(Main.class.getClassLoader().getResourceAsStream("./druid.properties"));
                                //使用工厂方法获取连接池对象
                                DataSource cpds = DruidDataSourceFactory.createDataSource(pro);
                                Connection conn = null;
                                try{
                                conn = cpds.getConnection();
                                //正常使用......
                                } catch(Exception e){

                                } finally{
                                if (conn != null){
                                try {
                                //正常使用关闭方法
                                conn.close();
                                } catch (SQLException e) {
                                throw new RuntimeException(e);
                                }
                                }
                                }
                                + +

                                定义工具类

                                一般使用的时候还是会自定义一个工具类的

                                +
                                import com.alibaba.druid.pool.DruidDataSourceFactory;

                                import javax.sql.DataSource;
                                import java.sql.Connection;
                                import java.sql.ResultSet;
                                import java.sql.SQLException;
                                import java.sql.Statement;
                                import java.util.Properties;

                                public class JDBCUtils {
                                private static DataSource ds;
                                static{
                                try {
                                Properties pro = new Properties();
                                pro.load(JDBCUtils.class.getClassLoader().getResourceAsStream("./druid.properties"));
                                ds = DruidDataSourceFactory.createDataSource(pro);
                                } catch (Exception e) {
                                throw new RuntimeException(e);
                                }
                                }

                                public static Connection getConnection() throws SQLException {
                                return ds.getConnection();
                                }

                                public static DataSource getDataSource(){
                                return ds;
                                }

                                //重载机制
                                public static void close(Connection conn){
                                close(conn,null,null);
                                }

                                public static void close(Connection conn, Statement stmt){
                                close(conn,stmt,null);
                                }

                                public static void close(Connection conn, Statement stmt, ResultSet resultSet){
                                if (resultSet != null){
                                try {
                                resultSet.close();
                                } catch (SQLException e) {
                                throw new RuntimeException(e);
                                }
                                }
                                if (stmt != null){
                                try {
                                stmt.close();
                                } catch (SQLException e) {
                                throw new RuntimeException(e);
                                }
                                }
                                if (conn !=null){
                                try {
                                conn.close();
                                } catch (SQLException e) {
                                throw new RuntimeException(e);
                                }
                                }
                                }
                                }
                                + +

                                使用同上的JDBCUtils

                                +

                                Spring JDBC

                                Spring框架对JDBC的简单封装,提供JDBCTemplate对象。

                                +

                                使用方法

                                带参(PreparedStatement)

                                jdbcTemplate.update("update usr set money = ? where uname = ?",10,"Mary");
                                + +

                                DML

                                update
                                public static void main(String[] args) throws Exception {
                                JdbcTemplate jdbcTemplate = new JdbcTemplate(JDBCUtils.getDataSource());

                                //增
                                int count = jdbcTemplate.update("insert into usr values (null,'Jack',3000),(null,'LiMing',500000)");
                                System.out.println(count);
                                //删
                                int count2 = jdbcTemplate.update("delete from usr where uname = 'Jack'");
                                System.out.println(count2);
                                //改
                                int count3 = jdbcTemplate.update("update usr set money = ? where uname = ?",10,"Mary");
                                System.out.println(count3);
                                }
                                /*输出结果:
                                2
                                1
                                1
                                */
                                + +

                                DQL

                                提供了三种方法。

                                +
                                queryForMap

                                将得到的结果(只能是一行)封装为一个Map<String,Object>,其中key为列名,value为该行该列的值。

                                +

                                如果得到的结果不为1行(=0 or >1),会抛出异常。

                                +
                                    JdbcTemplate jdbcTemplate = new JdbcTemplate(JDBCUtils.getDataSource());

                                Map<String,Object> m = jdbcTemplate.queryForMap("select * from usr where id = 2");
                                System.out.println(m);
                                //输出:{id=2, uname=Lily, money=2000}
                                + +
                                queryForList

                                将得到的结果封装为List<Map<String,Object>>,其中一个Map为一行,多个Map表示多行,存储在List中。

                                +
                                    List<Map<String, Object>> res = jdbcTemplate.queryForList("select * from usr");
                                for (Map<String,Object> m : res){
                                System.out.println(m.hashCode());
                                System.out.println(m.keySet().toString());
                                System.out.println(m.values().toString());
                                }
                                /*输出结果:
                                213151839
                                [id, uname, money]
                                [1, Mary, 10]
                                213143443
                                [id, uname, money]
                                [2, Lily, 2000]
                                -2022948335
                                [id, uname, money]
                                [4, LiMing, 500000]
                                */
                                + +
                                query

                                可以把查询回的结果封装为自己想要的对象而不是Map。如示例就封装为了Client对象。

                                +
                                原始一点的

                                可以看到,里面的包装内容还是得自己写,有点麻烦。

                                +
                                        JdbcTemplate jdbcTemplate = new JdbcTemplate(JDBCUtils.getDataSource());

                                List<Client> clients = jdbcTemplate.query(
                                "select * from usr where money > 1000",
                                new RowMapper<Client>() {
                                @Override
                                public Client mapRow(ResultSet resultSet, int i) throws SQLException {
                                return new Client(
                                resultSet.getInt(1),
                                resultSet.getString(2),
                                resultSet.getInt(3));
                                }
                                });
                                System.out.println(clients.toString());
                                /*输出结果
                                [Client{id=2, name='Lily', money=2000}, Client{id=4, name='LiMing', money=500000}, Client{id=6, name='LiMing', money=500000}]
                                */
                                + +
                                常用的

                                使用包装好的BeanPropertyRowMapper类。

                                +
                                        JdbcTemplate jdbcTemplate = new JdbcTemplate(JDBCUtils.getDataSource());

                                List<Client> clients = jdbcTemplate.query("select * from usr where money > 1000",
                                new BeanPropertyRowMapper<Client>(Client.class));
                                System.out.println(clients.toString());
                                /*输出结果:
                                [Client{id=2, uname='Lily', money=2000}, Client{id=4, uname='LiMing', money=500000}, Client{id=6, uname='LiMing', money=500000}]
                                */
                                + +

                                注意:

                                  -
                                1. 收集标识符的属性信息,并将其存入符号表
                                2. +
                                3. 要求包装的class,比如说Client,必须要有public的无参构造器
                                4. +
                                5. Java的那个被包装类的字段最好使用基本数据类型,而使用引用类型,如Integer,Double等等等。因为如果使用基本数据类型,当表中数据为null时会报错。
                                6. +
                                7. 要求被包装的class的字段名称一定要与数据库的一模一样,大小写可以不一样。
                                8. +
                                9. 要求被包装的class的字段一定要是可以修改的。也就是说,要么public,要么提供set方法。
                                -

                                picture

                                -

                                种属就是比如是函数还是数组之类的。

                                -

                                picture

                                -
                                  -
                                1. 语义检查
                                2. +
                                  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. 用户体验好
                                        -

                                        picture

                                        -
                                          -
                                        1. 静态绑定

                                          -

                                          包括绑定代码相对地址(子程序)、数据相对地址(变量)

                                        2. +
                                        3. 缺点:
                                            +
                                          1. 开发、安装,部署,维护 麻烦
                                          -

                                          中间代码生成

                                          picture

                                          -

                                          picture

                                          -

                                          波兰也就是前序遍历二叉树(中左右),逆波兰也就是后序遍历二叉树(左右中)

                                          -

                                          picture

                                          -

                                          代码优化

                                          picture

                                          -
                                            -
                                          1. 无关机器

                                            -

                                            picture

                                          2. -
                                          3. 有关机器

                                            -

                                            picture

                                            +
                                    2. +
                                    3. B/S: Browser/Server 浏览器/服务器端
                                        +
                                      • 只需要一个浏览器,用户通过不同的网址(URL),客户访问不同的服务器端程序
                                      • +
                                      • 优点:
                                          +
                                        1. 开发、安装,部署,维护 简单
                                        -

                                        目标代码生成

                                        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. + +
                                        9. 缺点:
                                            +
                                          1. 如果应用过大,用户的体验可能会受到影响
                                          2. +
                                          3. 对硬件要求过高
                                          -

                                          image-20230912153726618

                                          -

                                          帅的。

                                          -

                                          移植

                                          picture

                                          -

                                          picture

                                          -

                                          本机编译器的利用

                                          picture

                                          -

                                          编译程序的自动生成

                                          这大概是描述了我们到时候会怎么实现这两个阶段代码。

                                          -

                                          不过确实,词法分析可以看作是正则匹配,语法分析可以看作是产生式。

                                          -

                                          picture

                                          -

                                          picture

                                          -

                                          第二章 文法等概念

                                          image-20231111160656018

                                          -

                                          基本概念

                                            -
                                          1. 字母表

                                            -

                                            picture

                                            -

                                            picture

                                            -

                                            picture

                                            -

                                            picture

                                          2. -
                                          3. -

                                            克林闭包中的每一个元素都称为是字母表Σ上的一个串

                                            -

                                            picture

                                            -

                                            picture

                                            -

                                            picture

                                            +
                                    -

                                    文法

                                    picture

                                    -

                                    如果文法用于描述单词,基本符号就是字母;用于描述句子,基本符号就是单词

                                    -
                                      -
                                    1. 文法的形式化定义

                                      -

                                      picture

                                      -

                                      picture

                                      -

                                      由于可以从它们推出其他语法成分,故而称之为非终结符

                                      -

                                      picture

                                      -

                                      picture

                                      -

                                      还真是最大的语法成分

                                    2. -
                                    3. 产生式

                                      -

                                      picture

                                      -
                                    4. -
                                    5. 符号约定

                                      -

                                      picture

                                      -

                                      picture

                                      -

                                      picture

                                      -

                                      文法符号串应该就是指既包含终结符也包含非终结符的,也可能是空串的串。

                                      -

                                      注意终结符号串也包括空串。

                                      -
                                    6. -
                                    -

                                    语言

                                    picture

                                    -

                                    这部分就是要讲怎么看一个串是否满足文法规则,那么我们就需要先从什么样的串是满足文法规则的串开始说起,也即引入“语言”的概念。

                                    +
                                  • B/S架构详解

                                    +
                                      +
                                    • 资源分类:

                                        -
                                      1. 推导与归约

                                        -

                                        picture

                                        -

                                        然后也分为最左推导和最右推导,对应最右归约和最左归约。

                                        -

                                        picture

                                        -

                                        故而,如果从开始符号可以推导(派生)出该句子,或者从该句子可以归约到开始符号,那么该句子就是该语言的句子。

                                        +
                                      2. 静态资源:
                                          +
                                        • 使用静态网页开发技术发布的资源。
                                        • +
                                        • 特点:
                                            +
                                          • 所有用户访问,得到的结果是一样的。
                                          • +
                                          • 如:文本,图片,音频、视频, HTML,CSS,JavaScript
                                          • +
                                          • 如果用户请求的是静态资源,那么服务器会直接将静态资源发送给浏览器。浏览器中内置了静态资源的解析引擎,可以展示静态资源
                                          • +
                                        • -
                                        • 句子与句型

                                          -

                                          picture

                                          -

                                          句型就是可以有非终结符,句子就是只能有终结符

                                          +
                                      3. -
                                      4. 语言

                                        -

                                        picture

                                        -

                                        文法解决了无穷语言的有穷表示问题。

                                        -

                                        picture

                                        -

                                        picture

                                        -

                                        emm,就是好像没有∩运算

                                        -

                                        picture

                                        -

                                        有正则那味了

                                        +
                                      5. 动态资源:
                                          +
                                        • 使用动态网页及时发布的资源。
                                        • +
                                        • 特点:
                                            +
                                          • 所有用户访问,得到的结果可能不一样。
                                          • +
                                          • 如:jsp/servlet,php,asp…
                                          • +
                                          • 如果用户请求的是动态资源,那么服务器会执行动态资源,转换为静态资源,再发送给浏览器
                                          • +
                                          +
                                        • +
                                      -

                                      乔姆斯基文法体系

                                      picture

                                      -

                                      picture

                                      -
                                        -
                                      1. 0型

                                        -

                                        picture

                                      2. -
                                      3. 1型

                                        -

                                        picture

                                        -

                                        之所以是上下文有关,是因为只有A的上下文为a1和a2时才能替换为β【666666,第一次懂】

                                        -

                                        CSG不包含空产生式。

                                        +
                                      4. 我们要学习动态资源,必须先学习静态资源!

                                      5. -
                                      6. 2型

                                        -

                                        picture

                                        -

                                        左部只能是一个非终结符。

                                        +
                                      7. 静态资源:

                                        +
                                          +
                                        • HTML:用于搭建基础网页,展示页面的内容
                                        • +
                                        • CSS:用于美化页面,布局页面
                                        • +
                                        • JavaScript:控制页面的元素,让页面有一些动态的效果
                                        • +
                                      8. -
                                      9. 3型

                                        -

                                        picture

                                        -

                                        产生式右部最多只有一个非终结符,且要在同一侧

                                        -

                                        picture

                                        -

                                        看起来还能转(是的,自动机教的已经全忘了())

                                        +
                                  • -
                                -

                                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集元素匹配

                                  -

                                  直接用那个产生式

                                  + +

                                  静态网页概述

                                  练习:用纯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. 否则,看输入符号是否与Follow集元素匹配

                                    -
                                      -
                                    1. -

                                      若B无空产生式,报错;否则,使用B的空产生式(相当于消了一个符号但不变输入带指针)

                                      +
                                    2. 图片适应屏幕宽度

                                      +

                                      只需在img标签加个width=”100%”的属性即可。如:

                                      +
                                      <img src="./image/top_banner.jpg" width="100%">
                                    3. +
                                    +

                                    表单

                                    注意

                                      +
                                    1. 表项中的数据要被提交的话,必须指定其名称

                                      +

                                      image-20221223161847622

                                      +

                                      也即一定要有属性name。

                                    2. -
                                    3. -

                                      报错

                                      +
                                    4. 关于from的属性

                                      +

                                      image-20221223162035569

                                    5. -
                                    +
                                  4. 一般都这么写

                                    +

                                    image-20221223165046277

                                  -

                                  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(α)

                                    +

                                    练习

                                    image-20221223171603366

                                    +
                                    <!DOCTYPE html>
                                    <html lang="ch">
                                    <head>
                                    <meta charset="UTF-8">
                                    <title>注册界面</title>
                                    </head>
                                    <body>
                                    <form action="#">
                                    <table border="1" cellpadding="1" cellspacing="1">
                                    <tr>
                                    <td>
                                    <label for="username">用户名:</label>
                                    </td>
                                    <td>
                                    <input type="text" id="username" name="username" placeholder="请输入用户名">
                                    </td>
                                    </tr>
                                    <tr>
                                    <td>
                                    <label for="password">密码:</label>
                                    </td>
                                    <td>
                                    <input type="password" id="password" name="password" placeholder="请输入密码">
                                    </td>
                                    </tr>
                                    <tr>
                                    <td>
                                    <label for="email">邮箱:</label>
                                    </td>
                                    <td>
                                    <input type="email" id="email" name="email" placeholder="请输入邮箱">
                                    </td>
                                    </tr>
                                    <tr>
                                    <td>
                                    <label for="name">姓名:</label>
                                    </td>
                                    <td>
                                    <input type="text" id="name" name="name" placeholder="请输入姓名">
                                    </td>
                                    </tr>
                                    <tr>
                                    <td>
                                    <label for="phone_number">手机号:</label>
                                    </td>
                                    <td>
                                    <input type="text" id="phone_number" name="phone_number" placeholder="请输入手机号">
                                    </td>
                                    </tr>
                                    <tr>
                                    <td>
                                    性别:
                                    </td>
                                    <td>
                                    <input type="radio" name="gender" value="1">
                                    <input type="radio" name="gender" value="2">
                                    </td>
                                    </tr>
                                    <tr>
                                    <td>
                                    <label for="birthday">出生日期:</label>
                                    </td>
                                    <td>
                                    <input type="date" name="birthday" id="birthday">
                                    </td>
                                    </tr>
                                    <tr>
                                    <td>
                                    <label for="certification">验证码:</label>
                                    </td>
                                    <td>
                                    <input type="text" name="certification" id="certification">
                                    <img src="./image/verify_code.jpg" >
                                    </td>
                                    </tr>
                                    <tr align="center">
                                    <td colspan="2">
                                    <input type="image" src="./image/regbtn.jpg">
                                    </td>
                                    </tr>
                                    </table>
                                    </form>

                                    </body>
                                    </html>
                                    + +

                                    CSS

                                    盒子模型

                                    image-20221223204733649

                                    +

                                    练习:注册页面

                                    image-20221223223128861

                                    +
                                    <!DOCTYPE html>
                                    <html lang="ch">
                                    <head>
                                    <meta charset="UTF-8">
                                    <title>注册界面</title>
                                    <link rel="stylesheet" href="./2.css">
                                    </head>
                                    <body>
                                    <div id="log_in_box">
                                    <div id="log_in_text">
                                    <div id="log_in_text1">
                                    新用户注册
                                    </div>
                                    <div id="log_in_text2">
                                    USER REGISTER
                                    </div>
                                    </div>

                                    <div id="log_in_text3">
                                    已有账号?<font color = red>立即登录</font>
                                    </div>
                                    <div id="log_in_table">
                                    <form>
                                    <table>
                                    <tr>
                                    <td class="td_left"><label for="username">用户名</label></td>
                                    <td class="td_right"><input type="text" name="username" id="username" placeholder="请输入用户名"></td>
                                    </tr>

                                    <tr>
                                    <td class="td_left"><label for="password">密码</label></td>
                                    <td class="td_right"><input type="password" name="password" id="password" placeholder="请输入密码"></td>
                                    </tr>

                                    <tr>
                                    <td class="td_left"><label for="email">Email</label></td>
                                    <td class="td_right"><input type="email" name="email" id="email" placeholder="请输入邮箱"></td>
                                    </tr>

                                    <tr>
                                    <td class="td_left"><label for="name">姓名</label></td>
                                    <td class="td_right"><input type="text" name="name" id="name" placeholder="请输入姓名"></td>
                                    </tr>

                                    <tr>
                                    <td class="td_left"><label for="tel">手机号</label></td>
                                    <td class="td_right"><input type="text" name="tel" id="tel" placeholder="请输入手机号"></td>
                                    </tr>

                                    <tr>
                                    <td class="td_left"><label>性别</label></td>
                                    <td class="td_right">
                                    <input type="radio" name="gender" value="male"> <span class="choice"></span>
                                    <input type="radio" name="gender" value="female"> <span class="choice"></span>
                                    </td>
                                    </tr>

                                    <tr>
                                    <td class="td_left"><label for="birthday">出生日期</label></td>
                                    <td class="td_right"><input type="date" name="birthday" id="birthday" placeholder="请输入出生日期"></td>
                                    </tr>

                                    <tr>
                                    <td class="td_left"><label for="checkcode" >验证码</label></td>
                                    <td class="td_right"><input type="text" name="checkcode" id="checkcode" placeholder="请输入验证码">
                                    <img id="img_check" src="img/verify_code.jpg">
                                    </td>
                                    </tr>


                                    <tr>
                                    <td colspan="2" align="center"><input type="submit" value="注册" id="submit"></td>
                                    </tr>
                                    </table>
                                    </form>
                                    </div>
                                    </div>
                                    </body>
                                    </html>
                                    + +
                                    *{
                                    margin: 0px;
                                    padding: 0px;
                                    /*防止大小因padding变化*/
                                    box-sizing: border-box;
                                    }
                                    body{
                                    z-index: 0;
                                    background-image: url("./img/login_bg.png");
                                    }
                                    #log_in_box {
                                    border: 9px solid darkgray;
                                    z-index: 100;
                                    width: 987px;
                                    height: 590px;
                                    /*让div水平居中*/
                                    margin: auto;
                                    margin-top: 20px;
                                    padding: 15px;
                                    background: white;

                                    }

                                    #log_in_table {
                                    margin-top: 96px;
                                    margin-left: 300px;
                                    }

                                    #log_in_text3{
                                    font-size: 12px;
                                    float: right;
                                    }

                                    #log_in_text > div:first-child{
                                    margin-right: 0px;
                                    width: 150px;
                                    color: orange;
                                    font-size: 25px;
                                    }
                                    #log_in_text > div:last-child{
                                    margin-right: 0px;
                                    width: 200px;
                                    color: darkgray;
                                    font-size: 17px;
                                    font-family: "Arial Black";
                                    }

                                    #log_in_text{
                                    margin: 0px;
                                    display:inline;
                                    float: left;
                                    }

                                    /*此处一定得是选择input,不能是选择.td_right,因为这样才能覆盖掉input原有的那个丑边框*/
                                    input {
                                    padding: 5px;
                                    align-self: center;
                                    border: 1px solid lightgrey;
                                    height: 35px;
                                    border-radius: 6px;
                                    margin: 3px;
                                    margin-left: 15px;
                                    /*解决了radio的选框和文本不对齐。*/
                                    vertical-align: middle;
                                    }

                                    .td_left{
                                    color: slategrey;
                                    text-align: right;
                                    }

                                    .choice{
                                    font-size: 15px;
                                    }

                                    #checkcode{
                                    width: 100px;
                                    font-size: 15px;
                                    }

                                    #img_check{
                                    vertical-align:middle;
                                    }

                                    #submit{
                                    margin-top: 15px;
                                    margin-left: 15px;
                                    margin-right: 155px;
                                    color: transparent;
                                    width: 100px;
                                    background-image: url("./img/regbtn.jpg");
                                    }
                                    + +

                                    JavaScript

                                    对象

                                    function
                                    1. 创建:
                                    +   1. var fun = new Function(形式参数列表,方法体);  //忘掉吧
                                    +   2. function 方法名称(形式参数列表){
                                    +          方法体
                                    +      }
                                    +
                                    +   3. var 方法名 = function(形式参数列表){
                                    +           方法体
                                    +      }
                                    +2. 方法:
                                    +
                                    +3. 属性:
                                    +   length:代表形参的个数
                                    +4. 特点:
                                    +   1. 方法定义是,形参的类型不用写,返回值类型也不写。
                                    +   2. 方法是一个对象,如果定义名称相同的方法,会覆盖
                                    +   3. 在JS中,方法的调用只与方法的名称有关,和参数列表无关
                                    +   4. 在方法声明中有一个隐藏的内置对象(数组),**arguments**,封装所有的实际参数
                                    +5. 调用:
                                    +   方法名称(实际参数列表);
                                    +
                                    +
                                    /**
                                    * 求任意个数的和
                                    */
                                    function add (){
                                    var sum = 0;
                                    for (var i = 0; i < arguments.length; i++) {
                                    sum += arguments[i];
                                    }
                                    return sum;
                                    }

                                    var sum = add(1,2,3,4);
                                    alert(sum);
                                    + +
                                    Global
                                      +
                                    1. 特点:全局对象,这个Global中封装的方法不需要对象就可以直接调用。 方法名();

                                    2. -
                                    3. 非空产生式

                                      -

                                      很简单,就是其First集。

                                      +
                                    4. 方法:
                                      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);
                                    -

                                    故而,只需要让这些计算出来的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. 待约状态

                                      -

                                      ·后为非终结符

                                      +

                                      DOM

                                      image-20221224162341993

                                      +

                                      image-20221224162540306

                                      +
                                      document
                                      获取元素对象
                                      getElementById();
                                      getElementsByTagName();//通过标签名
                                      getElementsByClassName();
                                      getElementsByName();//注意是数组
                                      + +
                                      创建DOM对象
                                      createAttribute(name);
                                      createComment();
                                      createElement();
                                      createTextNode();
                                      + +
                                      Element
                                      removeAttribute();
                                      setAttribute(属性名,属性值);
                                      + +
                                      Node

                                      说了树结构后,这个就好理解多了。

                                      +

                                      image-20221224164239507

                                      +
                                      练习:动态表格
                                      <!DOCTYPE html>
                                      <html lang="ch">
                                      <head>
                                      <meta charset="UTF-8">
                                      <title>动态表格</title>
                                      <style>
                                      #input{
                                      width: 60%;
                                      margin: auto;
                                      margin-top: 50px;
                                      }
                                      table{
                                      margin: auto;
                                      margin-top: 100px;
                                      width: 70%;
                                      text-align: center;
                                      }
                                      </style>
                                      </head>
                                      <body>
                                      <div id="input">
                                      <input type="text" placeholder="请输入编号" id="id">
                                      <input type="text" placeholder="请输入姓名" id="name">
                                      <input type="text" placeholder="请输入性别" id="gender">
                                      <input type="button" id="add_but" value="添加">
                                      </div>
                                      <table border="1px solid black" id="table">
                                      <tr>
                                      <title>学生信息表</title>
                                      <td>编号</td>
                                      <td>姓名</td>
                                      <td>性别</td>
                                      <td>操作</td>
                                      </tr>
                                      </table>
                                      <script>
                                      //获取表对象
                                      let table = document.getElementById("table");

                                      //最后那列删除要用很多次,所以这里只写一份原始的,之后要用再copy del_col对象即可。
                                      //但是要注意,js的深拷贝 object.cloneNode(true)是不会拷贝事件绑定的
                                      //所以事件绑定不得不放在下面的函数里做了
                                      let del_col = document.createElement("td");
                                      let del_col_a = document.createElement("a");
                                      del_col_a.href="javascript:void(0);";
                                      del_col_a.innerHTML="删除";
                                      del_col.appendChild(del_col_a);

                                      //删除列
                                      function del_row(obj){
                                      let target = obj.parentNode.parentNode;
                                      table.removeChild(target);
                                      }
                                      //添加行
                                      function add_row(id,name,gender){
                                      if (id=="" || name=="" || gender=="") return;
                                      let row = document.createElement("tr");
                                      let col1 = document.createElement("td");
                                      let col2 = document.createElement("td");
                                      let col3 = document.createElement("td");
                                      col1.innerHTML = id;
                                      col2.innerHTML = name;
                                      col3.innerHTML = gender;

                                      //为“删除”绑定事件
                                      let col4 = del_col.cloneNode(true);
                                      col4.lastChild.onclick = function(){
                                      //通过this定位。此时this指代a标签。
                                      //如果在del_row内把obj换成this反倒是不行的,因为那时候的this会指代的是window
                                      del_row(this);
                                      };

                                      row.appendChild(col1);
                                      row.appendChild(col2);
                                      row.appendChild(col3);
                                      row.appendChild(col4);

                                      table.appendChild(row);
                                      }

                                      //为“添加”按钮绑定事件
                                      document.getElementById("add_but").onclick = function(){
                                      add_row(document.getElementById("id").value,
                                      document.getElementById("name").value,
                                      document.getElementById("gender").value);
                                      };
                                      </script>
                                      </body>
                                      </html>
                                      + +

                                      老师标答值得学习借鉴的点:

                                      +
                                      //使用innerHTML添加
                                      document.getElementById("btn_add").onclick = function() {
                                      //2.获取文本框的内容
                                      var id = document.getElementById("id").value;
                                      var name = document.getElementById("name").value;
                                      var gender = document.getElementById("gender").value;

                                      //获取table
                                      var table = document.getElementsByTagName("table")[0];

                                      //追加一行
                                      table.innerHTML += "<tr>\n" +
                                      " <td>"+id+"</td>\n" +
                                      " <td>"+name+"</td>\n" +
                                      " <td>"+gender+"</td>\n" +
                                      " <td><a href=\"javascript:void(0);\" onclick=\"delTr(this);\" >删除</a></td>\n" +
                                      " </tr>";
                                      }
                                      + +
                                      事件
                                      练习:全选/全不选/反选+行变色

                                      image-20221225192124809

                                      +
                                      <!DOCTYPE html>
                                      <html lang="ch">
                                      <head>
                                      <meta charset="UTF-8">
                                      <title>动态表格</title>
                                      <style>
                                      #input{
                                      width: 60%;
                                      margin: auto;
                                      margin-top: 50px;
                                      }
                                      table{
                                      margin: auto;
                                      margin-top: 100px;
                                      width: 70%;
                                      text-align: center;
                                      }
                                      </style>
                                      </head>
                                      <body>
                                      <div id="input">
                                      <input type="text" placeholder="请输入编号" id="id">
                                      <input type="text" placeholder="请输入姓名" id="name">
                                      <input type="text" placeholder="请输入性别" id="gender">
                                      <input type="button" id="add_but" value="添加">
                                      </div>
                                      <table border="1px solid black" id="table">
                                      <tr>
                                      <td>选择</td>
                                      <td>编号</td>
                                      <td>姓名</td>
                                      <td>性别</td>
                                      <td>操作</td>
                                      </tr>
                                      <tr>
                                      <td><input type="checkbox" class="box"></td>
                                      <td>1</td>
                                      <td>Lily</td>
                                      <td>female</td>
                                      <td><a href="#" >删除</a></td>
                                      </tr>
                                      <tr>
                                      <td><input type="checkbox" class="box"></td>
                                      <td>2</td>
                                      <td>Jack</td>
                                      <td>male</td>
                                      <td><a href="#" >删除</a></td>
                                      </tr>
                                      <tr>
                                      <td><input type="checkbox" class="box"></td>
                                      <td>3</td>
                                      <td>Peterson</td>
                                      <td>male</td>
                                      <td><a href="#" >删除</a></td>
                                      </tr>
                                      <tr>
                                      <td><input type="checkbox" class="box"></td>
                                      <td>4</td>
                                      <td>Mary</td>
                                      <td>female</td>
                                      <td><a href="#" >删除</a></td>
                                      </tr>
                                      </table>
                                      <div style="text-align: center;margin-top: 20px">
                                      <input type="button" id="select_all" value="全选">
                                      <input type="button" id="select_none" value="全不选">
                                      <input type="button" id="select_aside" value="反选">
                                      </div>
                                      <script>
                                      window.onload = function (){
                                      //行变色
                                      let rows = document.getElementsByTagName("tr");
                                      for (let i = 0; i < rows.length; i++) {
                                      rows[i].onmouseover = function (){
                                      rows[i].setAttribute("style","background: pink");
                                      };
                                      rows[i].onmouseout = function (){
                                      rows[i].removeAttribute("style");
                                      }
                                      }

                                      //三个按钮
                                      let select_all = document.getElementById("select_all");
                                      let select_none = document.getElementById("select_none");
                                      let select_aside = document.getElementById("select_aside");
                                      let boxes = document.getElementsByClassName("box");

                                      select_all.onclick = function(){
                                      for (let i = 0; i < boxes.length; i++) {
                                      boxes[i].checked = 1;
                                      }
                                      };
                                      select_none.onclick = function(){
                                      for (let i = 0; i < boxes.length; i++) {
                                      boxes[i].checked = 0;
                                      }
                                      };
                                      select_aside.onclick = function(){
                                      for (let i = 0; i < boxes.length; i++) {
                                      boxes[i].checked = !(boxes[i].checked);
                                      }
                                      };
                                      };
                                      </script>
                                      </body>
                                      </html>
                                      + +
                                      练习:表单校验
                                      <!DOCTYPE html>
                                      <html lang="ch">
                                      <head>
                                      <meta charset="UTF-8">
                                      <title>注册界面</title>
                                      <link rel="stylesheet" href="./2.css">
                                      </head>
                                      <body>
                                      <div id="log_in_box">
                                      <div id="log_in_text">
                                      <div id="log_in_text1">
                                      新用户注册
                                      </div>
                                      <div id="log_in_text2">
                                      USER REGISTER
                                      </div>
                                      </div>

                                      <div id="log_in_text3">
                                      已有账号?<font color = red>立即登录</font>
                                      </div>
                                      <div id="log_in_table">
                                      <form id="form">
                                      <table>
                                      <tr>
                                      <td class="td_left"><label for="username">用户名</label></td>
                                      <td class="td_right"><input type="text" name="username" id="username" placeholder="请输入用户名"></td>
                                      <td></td>
                                      </tr>

                                      <tr>
                                      <td class="td_left"><label for="password">密码</label></td>
                                      <td class="td_right"><input type="password" name="password" id="password" placeholder="请输入密码"></td>
                                      <td></td>
                                      </tr>

                                      <tr>
                                      <td class="td_left"><label for="email">Email</label></td>
                                      <td class="td_right"><input type="email" name="email" id="email" placeholder="请输入邮箱"></td>
                                      </tr>

                                      <tr>
                                      <td class="td_left"><label for="name">姓名</label></td>
                                      <td class="td_right"><input type="text" name="name" id="name" placeholder="请输入姓名"></td>
                                      </tr>

                                      <tr>
                                      <td class="td_left"><label for="tel">手机号</label></td>
                                      <td class="td_right"><input type="text" name="tel" id="tel" placeholder="请输入手机号"></td>
                                      </tr>

                                      <tr>
                                      <td class="td_left"><label>性别</label></td>
                                      <td class="td_right">
                                      <input type="radio" name="gender" value="male"> <span class="choice"></span>
                                      <input type="radio" name="gender" value="female"> <span class="choice"></span>
                                      </td>
                                      </tr>

                                      <tr>
                                      <td class="td_left"><label for="birthday">出生日期</label></td>
                                      <td class="td_right"><input type="date" name="birthday" id="birthday" placeholder="请输入出生日期"></td>
                                      </tr>

                                      <tr>
                                      <td class="td_left"><label for="checkcode" >验证码</label></td>
                                      <td class="td_right"><input type="text" name="checkcode" id="checkcode" placeholder="请输入验证码">
                                      <img id="img_check" src="img/verify_code.jpg">
                                      </td>
                                      </tr>


                                      <tr>
                                      <td colspan="2" align="center"><input type="submit" value="注册" id="submit"></td>
                                      </tr>
                                      </table>
                                      </form>
                                      </div>
                                      </div>

                                      <script>
                                      //在开始加载的时候绑定事件的好习惯
                                      window.onload = function(){
                                      document.getElementById("form").onsubmit = function (){
                                      return checkUsername()&&checkPassword();
                                      };
                                      document.getElementById("username").onblur = checkUsername;
                                      document.getElementById("password").onblur = checkPassword;
                                      }
                                      let username = document.getElementById("username");
                                      let password = document.getElementById("password");

                                      function checkUsername(){
                                      let reg_username = /^\w{6,12}$/;
                                      let flag = reg_username.test(username.value);
                                      if (flag){
                                      username.parentNode.parentNode.lastElementChild.innerHTML = "<img src=\"./img/gou.png\" alt=\"\">";
                                      }
                                      else{
                                      username.parentNode.parentNode.lastElementChild.innerHTML = "<font color=\"red\">格式错误!</font>";
                                      }
                                      return flag;
                                      }

                                      function checkPassword(){
                                      let reg_username = /^\w{6,12}$/;
                                      let flag = reg_username.test(password.value);
                                      if (flag){
                                      password.parentNode.parentNode.lastElementChild.innerHTML = "<img src=\"./img/gou.png\" alt=\"\">";
                                      }
                                      else{
                                      password.parentNode.parentNode.lastElementChild.innerHTML = "<font color=\"red\">格式错误!</font>";
                                      }
                                      return flag;
                                      }

                                      </script>
                                      </body>
                                      </html>
                                      + +

                                      BOM

                                      浏览器对象模型,将浏览器各个组成部分封装成对象。

                                      +

                                      image-20221224152804747

                                      +

                                      Window对象包含DOM对象。

                                      +

                                      组成:Window、Navigator、Screen、History、Location

                                      +
                                      Window

                                      不需要创建,直接用window.使用,也可以直接用方法名。比如alert

                                      +
                                      方法
                                        +
                                      1. 与弹出有关的方法

                                        +

                                        alert:弹出警告框; confirm:确认取消对话框。确定返回true;prompt:输入框。参数为输入提示,返回值为输入值。

                                      2. -
                                      3. 归约状态

                                        -

                                        ·后为空

                                        +
                                      4. 与开关有关的方法

                                        +

                                        close:关闭调用的window对象的浏览器窗口;open:打开新窗口,可传入URL,返回新的window对象

                                      5. +
                                      6. 定时器

                                        +
                                        //只执行一次
                                        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);
                                      -

                                      picture

                                      -

                                      picture

                                      -

                                      以前感觉一直很难理解GOTO表的作用,现在感觉稍微明白了点了,你想想,归约之后的那个结果是不是有可能是另一个产生式的右部成分之一,也即一个新的句柄?并且这个也是由你栈顶刚归约好的那个左部和下面的输入符号决定的。那么你自然而然需要切换一下当前状态,以便之后遇到那个产生式的时候能发现到了。

                                      -

                                      那么,剩下的问题就是如何构造LR分析表了:

                                      -

                                      picture

                                      -

                                      算符分析

                                      picture

                                      -

                                      也就是它会整一个终结符之间的优先级关系。。。

                                      -

                                      picture

                                      -

                                      picture

                                      -

                                      也就是说:

                                      -
                                        -
                                      1. a=b

                                        -

                                        相邻

                                        +
                                        属性
                                          +
                                        1. 获取其他BOM对象

                                          +

                                          history、location、navigator、screen

                                        2. -
                                        3. a<b

                                          -

                                          也即在A->aB时,b在FIRSTOP(B)中(理解一下,这个First指在前面。。。)

                                          +
                                        4. 获取DOM对象

                                          +

                                          document

                                        5. -
                                        6. a>b

                                          -

                                          也即在A->Bb时,a在LASTOP(B)中(理解一下,这个LAST指在后面。。。)

                                          +
                                        +
                                        练习:轮播图
                                        <!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属性

                                        -

                                        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. +
                                          练习:自动返回首页
                                          <!DOCTYPE html>
                                          <html lang="ch">
                                          <head>
                                          <meta charset="UTF-8">
                                          <title>自动跳转</title>
                                          </head>
                                          <body>
                                          <div style="width: 235px; height: 100px; margin: auto">
                                          <span style="color: red" id="second">5</span>秒后,自动跳转首页
                                          </div>
                                          <script>
                                          let num = 5;
                                          let second = document.getElementById("second");
                                          function dao_ji_shi(){
                                          if (num == 0){
                                          location.href = "https://www.baidu.com/";
                                          clearInterval(id);
                                          }
                                          second.innerHTML = (num--);
                                          }
                                          let id = setInterval(dao_ji_shi,1000);
                                          </script>
                                          </body>
                                          </html>
                                          + +

                                          Bootstrap

                                          web前端框架

                                          +

                                          image-20221225160246028

                                          +

                                          快速入门

                                          image-20221225161243229

                                          +

                                          基本模板:

                                          +
                                          <!DOCTYPE html>
                                          <html lang="zh-CN">
                                          <head>
                                          <meta charset="utf-8">
                                          <meta http-equiv="X-UA-Compatible" content="IE=edge">
                                          <meta name="viewport" content="width=device-width, initial-scale=1">
                                          <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
                                          <title>Bootstrap 101 Template</title>

                                          <link href="./css/bootstrap.min.css" rel="stylesheet">
                                          </head>
                                          <body>
                                          <h1>你好,世界!</h1>

                                          <script src="./jquery.min.js"></script>
                                          <script src="js/bootstrap.min.js"></script>
                                          </body>
                                          </html>
                                          + +

                                          响应式布局

                                          实现依赖于栅格系统。

                                          +

                                          栅格系统

                                          将一行平均分成12个格子,可以指定元素占几个格子

                                          +

                                          可以感受到,其实跟我们之前那个纯纯HTML做页面的思想是差不多的,都是把整个页面看做一个表,表有很多行,每行有不同的格子。

                                          +
                                          基本原理
                                            +
                                          1. 定义容器。相当于之前的table
                                          -

                                          思想:

                                          +
                                            +
                                          • 容器分类

                                              -
                                            1. 通过为文法符号设置语义属性,来表达语义信息
                                            2. -
                                            3. 通过与产生式(语法规则)相关联的语义规则来计算符号的语义属性值
                                            4. +
                                            5. container:两边留白
                                            6. +
                                            7. container-fluid:每一种设备都是100%宽度
                                            -

                                            也可能是先入为主吧,感觉用实验的方法来理解语义分析比较便利。语义分析相当于定义一连串事件,附加在每个产生式上。当该产生式进行归约的时候,就执行对应的语义事件。而由于执行语义分析时需要的符号在语法分析栈中,所以我们也同样需要维护一个语义分析栈,在移进时也需要进栈。

                                            -

                                            SDD/SDT概念

                                            语义分析一般与语法分析一同实现,这一技术成为语法制导翻译。

                                            -

                                            picture

                                            -

                                            picture

                                            -

                                            picture

                                            -

                                            SDD

                                            picture

                                            -

                                            可以回忆一下实验,相当于对每个产生式进行一个switch-case,然后依照产生式的类别和代码规则进行出栈入栈来计算属性值。

                                            -

                                            SDT

                                            picture

                                            -

                                            picture

                                            -

                                            SDD

                                            picture

                                            -

                                            概念

                                            一个很简单区分综合属性和继承属性的方法,就是如果定义的是产生式左部的属性,那就是综合属性;右部,那就是继承属性

                                            -

                                            综合属性

                                            picture

                                            -

                                            picture

                                            -

                                            继承属性

                                            picture

                                            -

                                            picture

                                            -

                                            这个东西就是我们实验里写的,副作用也是更新符号表。

                                            -

                                            属性文法

                                            没有副作用的SDD称为属性文法。

                                            -

                                            求值顺序

                                            picture

                                            -

                                            而感觉语法分析这个过程的产生式归约顺序就能一定程度上表示了这个求值顺序

                                            -

                                            picture

                                            +
                                          • +
                                          +
                                            +
                                          1. 定义行。相当于之前的tr 样式:row
                                          2. +
                                          3. 定义元素。指定该元素在不同的设备上,所占的格子数目。样式:col-设备代号-格子数目
                                          4. +
                                          +
                                            +
                                          • 设备代号:

                                              -
                                            1. 继承属性放在结点左边,综合属性放在结点右边
                                            2. -
                                            3. 如果属性值A依赖于属性值B,那么就有一条从B到A的箭头【B决定A】
                                            4. -
                                            5. 对于副作用,我们将其看作一个虚综合属性【注意是综合的,虽然它看起来既由兄弟结点决定也由子节点决定】
                                            6. -
                                            7. 可行的求值序列就是拓扑排序
                                            8. +
                                            9. xs:超小屏幕 手机 (<768px):col-xs-12
                                            10. +
                                            11. sm:小屏幕 平板 (≥768px)
                                            12. +
                                            13. md:中等屏幕 桌面显示器 (≥992px)
                                            14. +
                                            15. lg:大屏幕 大桌面显示器 (≥1200px)
                                            -

                                            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 +
                                          • +
                                          +
                                          使用方法
                                          <!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>
                                          -
                                          -

                                          关联的另一个实例:

                                          -

                                          image-20231117015508123

                                          -

                                          此时由于T’.inh还要被a3用到,所以我们就得在T’出栈前把它的这个inh值复制给a3。

                                          -
                                          -

                                          当遇到语义动作之后,就执行动作,并且出栈语义动作。

                                          -

                                          picture

                                          -

                                          它这意思应该是遇到每个产生式的每个符号要执行什么动作都是确定的,所以代码实现是可能的。

                                          -

                                          可以看到:

                                          +

                                          样式

                                          看文档。

                                          +

                                          练习:用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. 语义动作代码就是执行
                                          2. -
                                          3. 综合属性代码就是赋给关联语义动作
                                          4. -
                                          5. 非终结符就是选一个它作为左部的产生式,然后看看要不要用到它自身的属性对右部子属性进行复制(体现了继承属性)
                                          6. +
                                          7. text-align

                                            +

                                            是一个css属性,我觉得挺好用的(。我不知道它精确是什么意思,但我发现它好像有种能让该元素下的子元素水平居中的效果。

                                            +
                                          8. +
                                          9. 关于“容器”的理解

                                            +

                                            上面说过,Bootstrap有个容器的概念,跟我们上面纯HTML的表格概念其实是很类似的。

                                            +

                                            HTML的容器是表格标签,Bootsrap的容器是container-fluid和container类的标签。

                                            +

                                            与HTML的表格相同,“容器”也是可以嵌套的。这点在本案例体现为一下两点:

                                            +

                                            ① container中可以嵌套container-fluid。

                                            +

                                            ​ 案例中,页首-轮播图和页尾这两段是两边不留白的,轮播图-页尾这段是两边留白的。所以,我们就可以让整体为一个container容器,中间一段再用container-fluid容器包装起来。也即:

                                            +
                                            <body>
                                            <div class="container-fluid">
                                            <div class="row"></div>
                                            <div class="container"></div>
                                            <div class="row"></div>
                                            </div>
                                            </body>
                                            + +

                                            注意,此处不要作死为了优雅统一性这样写:

                                            +
                                            <div class="row container"></div>
                                            + +

                                            也即多加一个row类。要不row的属性会覆盖掉container的。

                                            +

                                            ② 对于“col-md-4”这些的理解

                                            +

                                            image-20221225183910393

                                            +

                                            在做这样的包含row-span元素的行时,之前的解决方案是采用表格嵌套。同样的,这里也可以采用容器嵌套。而此时,列的书写方式就比较特殊了。

                                            +
                                            <div class="row">
                                            <div class="col-md-4">
                                            row-span的图片
                                            </div>

                                            <div class="col-md-8">
                                            <div class="col-md-4"></div>
                                            <div class="col-md-4"></div>
                                            <div class="col-md-4"></div>
                                            <div class="col-md-4"></div>
                                            <div class="col-md-4"></div>
                                            <div class="col-md-4"></div>
                                            </div>
                                            </div>
                                            + +

                                            其实是非常直观的,相信以后你看到这段应该也能理解(。提示一点,栅格系统其实好像是相对于父类的。也就是说,不是“把整个页面分成12个格子”,而是,“把父类占有的空间分成12个格子”。

                                            +
                                          10. +
                                          11. 关于hr标签

                                            +

                                            使用css改颜色时应该写background: orange;而不是color: orange;

                                            +
                                          -

                                          递归的预测分析

                                          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/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是小端,也即低字节放在低地址

                                            +

                                            XML

                                            xml叫做可扩展标签语言。它的全部标签都是自定义的。

                                            +

                                            image-20221225220356932

                                            +

                                            快速入门

                                              +
                                            • 基本语法:
                                                1. xml文档的后缀名 .xml
                                              +  2. xml第一行必须定义为文档声明
                                              +  3. xml文档中有且仅有一个根标签
                                              +  4. 属性值必须使用引号(单双都可)引起来
                                              +  5. 标签必须正确关闭
                                              +  6. xml标签名称区分大小写
                                              +
                                            • -
                                            • 支持字节(8位)、半字(16位)、字(32位)、双字(64位,64位架构)的数据传输

                                              -

                                              主存按照字节进行编址

                                              +
                                            +
                                            <?xml version="1.0" encoding="utf-8" ?>

                                            <users>
                                            <user id="1">
                                            <name>zhangsan</name>
                                            <age>23</age>
                                            <gender>male</gender>
                                            </user>
                                            <user id="2">
                                            <name>zhangsan</name>
                                            <age>23</age>
                                            <gender>male</gender>
                                            </user>
                                            <user id="3">
                                            <name>zhangsan</name>
                                            <age>23</age>
                                            <gender>male</gender>
                                            </user>
                                            </users>
                                            + +

                                            细说

                                            语法

                                            文档声明

                                            常见属性:version[必须]、encoding、standalone[取值为yes和no,yes为依赖其他文件]

                                            +
                                            属性

                                            id属性值唯一

                                            +
                                            文本

                                            image-20221225222029698

                                            +

                                            这种要转义来转义去的显然很麻烦。所以就需要用到CDATA区。

                                            +

                                            CDATA区的文本会被原样展示。

                                            +
                                            <code>

                                            <![CDATA[
                                            if(1 == 1 && 2 == 2){}
                                            ]]>

                                            </code>
                                            + +

                                            约束

                                            只能写约束文件内的标签

                                            +
                                            dtd
                                            文档
                                            //students标签里可以包含若干个student标签
                                            <!ELEMENT students (student*) >
                                            //student标签必须按顺序出现name,age,sex标签
                                            <!ELEMENT student (name,age,sex)>
                                            //name、age、sex都为字符串类型
                                            <!ELEMENT name (#PCDATA)>
                                            <!ELEMENT age (#PCDATA)>
                                            <!ELEMENT sex (#PCDATA)>
                                            //student标签有一个属性叫number,类型为ID,并且必须要有。类型为ID表示该属性值唯一。
                                            <!ATTLIST student number ID #REQUIRED>
                                            + +
                                            引入方式
                                            //外部引入
                                            <!DOCTYPE 根标签名 SYSTEM "dtd文件的位置">
                                            <!DOCTYPE 根标签名 PUBLIC "dtd文件名字" "dtd文件的位置URL">
                                            + +

                                            或者可以直接在xml内部写:

                                            +
                                            <?xml version="1.0" encoding="UTF-8" ?>
                                            <!DOCTYPE students SYSTEM "student.dtd">

                                            <!DOCTYPE students [

                                            <!ELEMENT students (student+) >
                                            <!ELEMENT student (name,age,sex)>
                                            <!ELEMENT name (#PCDATA)>
                                            <!ELEMENT age (#PCDATA)>
                                            <!ELEMENT sex (#PCDATA)>
                                            <!ATTLIST student number ID #REQUIRED>


                                            ]>
                                            <students>

                                            <student number="s001">
                                            <name>zhangsan</name>
                                            <age>abc</age>
                                            <sex>hehe</sex>
                                            </student>

                                            <student number="s002">
                                            <name>lisi</name>
                                            <age>24</age>
                                            <sex>female</sex>
                                            </student>

                                            </students>
                                            + +
                                            使用
                                            <student number="s001">
                                            <name>zhangsan</name>
                                            <age>abc</age>
                                            <sex>hehe</sex>
                                            </student>

                                            <student number="s002">
                                            <name>lisi</name>
                                            <age>24</age>
                                            <sex>female</sex>
                                            </student>
                                            + +
                                            schema
                                            文档
                                            <?xml version="1.0"?>
                                            <xsd:schema xmlns="http://www.itcast.cn/xml"
                                            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
                                            targetNamespace="http://www.itcast.cn/xml" elementFormDefault="qualified">

                                            <xsd:element name="students" type="studentsType"/>
                                            //定义类型
                                            <xsd:complexType name="studentsType">
                                            //类型里面有有序序列
                                            <xsd:sequence>
                                            <xsd:element name="student" type="studentType" minOccurs="0" maxOccurs="unbounded"/>
                                            </xsd:sequence>
                                            </xsd:complexType>

                                            <xsd:complexType name="studentType">
                                            <xsd:sequence>
                                            <xsd:element name="name" type="xsd:string"/>
                                            <xsd:element name="age" type="ageType" />
                                            <xsd:element name="sex" type="sexType" />
                                            </xsd:sequence>
                                            <xsd:attribute name="number" type="numberType" use="required"/>
                                            </xsd:complexType>

                                            <xsd:simpleType name="sexType">
                                            <xsd:restriction base="xsd:string">
                                            //枚举类型
                                            <xsd:enumeration value="male"/>
                                            <xsd:enumeration value="female"/>
                                            </xsd:restriction>
                                            </xsd:simpleType>

                                            <xsd:simpleType name="ageType">
                                            <xsd:restriction base="xsd:integer">
                                            <xsd:minInclusive value="0"/>
                                            <xsd:maxInclusive value="256"/>
                                            </xsd:restriction>
                                            </xsd:simpleType>

                                            <xsd:simpleType name="numberType">
                                            <xsd:restriction base="xsd:string">
                                            //正则匹配
                                            <xsd:pattern value="heima_\d{4}"/>
                                            </xsd:restriction>
                                            </xsd:simpleType>
                                            </xsd:schema>
                                            + +
                                            引入方式
                                            <?xml version="1.0" encoding="UTF-8" ?>
                                            <!--
                                            1.填写xml文档的根元素
                                            2.引入xsi前缀. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                                            3.引入xsd文件命名空间. xsi:schemaLocation="http://www.itcast.cn/xml student.xsd"
                                            4.为每一个xsd约束声明一个前缀,作为标识 xmlns="http://www.itcast.cn/xml"


                                            -->
                                            <students xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                                            xmlns="http://www.itcast.cn/xml"
                                            xsi:schemaLocation="http://www.itcast.cn/xml student.xsd"
                                            >
                                            <student number="heima_0001">
                                            <name>tom</name>
                                            <age>18</age>
                                            <sex>male</sex>
                                            </student>

                                            </students>
                                            + +

                                            它这意识就是,每个schema文件都要起一个别名,比如xsi:schemaLocation="http://www.itcast.cn/xml student.xsd"这行代码实际上就是把student.xsd的别名起为了http://www.itcast.cn/xml

                                            +

                                            为什么要起名呢?这就类似于命名空间这种东西,你要用到一个标签需要指明这个标签从哪来的,比如std::vectorClass.toString这种。这种命名空间在xml里叫前缀。所以,实际上完整写法应该是<http://www.itcast.cn/xml:students>

                                            +

                                            但这样显然太麻烦了,别名一般都是这种网址,写起来太长了。所以我们选择给别名起别名,设置方法为xmlns:a="http://www.itcast.cn/xml",这样一来,以后就不用写<http://www.itcast.cn/xml:students>,只用写<a:students>了。

                                            +

                                            但是如果每个都写一个前缀还是有点难顶。所以就引入了一个空前缀。这样写<students>这样没有前缀的标签,就相当于从空前缀那个命名空间里拿出来的了。当然如果有多个命名空间,还是得区分一下的。

                                            +

                                            解析

                                            image-20221225234106078

                                            +

                                            解析方式有两种方法。

                                            +
                                            解析方法
                                            DOM

                                            将标记语言文档一次性加载进内存,形成DOM树

                                            +

                                            操作方便,可以进行CRUD所有操作;但占内存

                                            +
                                            SAX

                                            逐行读取,基于事件驱动

                                            +

                                            不占内存;但只能读取

                                            +
                                            解析工具

                                            image-20221225234649287

                                            +

                                            主要学习Jsoup。

                                            +
                                            快速入门

                                            image-20221225234813135

                                            +

                                            跟前面html的DOM是差不多的。

                                            +
                                            public class JsoupDemo {
                                            public static void main(String[] args) throws IOException {
                                            //获取Document对象,根据xml文档
                                            //解析xml文档(加载进内存且获取dom树)
                                            Document doc = Jsoup.parse(new File(JsoupDemo.class.getClassLoader()
                                            .getResource("student.xml").getPath()),"utf-8");
                                            //获取元素对象
                                            //Elements extends ArrayList<Element>
                                            Elements ele = doc.getElementsByTag("name");
                                            //获取元素里的数据
                                            System.out.println(ele.get(0).text());
                                            }
                                            }
                                            + +
                                            细说
                                              +
                                            1. Jsoup:工具类,可以解析html或xml文档,返回Document

                                              +
                                                +
                                              • parse:解析html或xml文档,返回Document
                                                  +
                                                • parse(File in, String charsetName):解析xml或html文件的。
                                                • +
                                                • parse(String html):解析xml或html字符串
                                                • +
                                                • parse(URL url, int timeoutMillis):通过网络路径获取指定的html或xml的文档对象
                                                • +
                                              • -
                                              • 采用哈佛结构

                                                +
                                            2. -
                                            3. 三种特权模式

                                              -

                                              image-20230617155657577

                                              +
                                            4. Document:文档对象。代表内存中的dom树

                                              +
                                                +
                                              • 获取Element对象
                                                  +
                                                • getElementById(String id):根据id属性值获取唯一的element对象
                                                • +
                                                • getElementsByTag(String tagName):根据标签名称获取元素对象集合
                                                • +
                                                • getElementsByAttribute(String key):根据属性名称获取元素对象集合
                                                • +
                                                • getElementsByAttributeValue(String key, String value):根据对应的属性名和属性值获取元素对象集合
                                                • +
                                              • -
                                              • 模块化设计

                                                -

                                                image-20230617155224429

                                                +
                                            5. +
                                            6. Elements:元素Element对象的集合。可以当做 ArrayList来使用

                                              +
                                            7. +
                                            8. Element:元素对象

                                              +
                                                +
                                              1. 获取子元素对象
                                              -

                                              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

                                              -

                                              一般栈顶地址最低。

                                              -
                                              -

                                              运算方法

                                              定点运算

                                              一位乘法运算

                                              原码一位乘

                                              -

                                              image-20230621195833297

                                              -

                                              大致明白了:

                                              -

                                              ①乘积一共有四位,故而需要两个寄存器来保存。

                                              -

                                              ②按照上面的原理公式,每次右移一位,被移出的那一位也是最后的结果(相当于竖式中每次相加的最后一位),需要把它存储在另一个寄存器中。

                                              -

                                              ③我们选择了存乘数的寄存器,因为乘数已经乘过的位是没用的。存乘数的那个寄存器的乘数不断被结果的低位所替代。

                                              -

                                              故****基本流程****:

                                              -

                                              ①准备阶段:清零ACC【置部分积=0】,在MQ中放乘数,X中放被乘数

                                              -

                                              ②判断MQ中乘数最低位,若为1,则ACC部分积加上X中的被乘数;若为0,则ACC不变

                                              -

                                              ③将ACC和MQ中四位数字视作一个整体,符号位也算上,进行逻辑右移,左侧补0.

                                              -

                                              ④重复上述过程,按移位次数来控制结束。

                                              -

                                              ⑤则最后,ACC中存储的就是乘法结果的高位,MQ中存储的结果就是乘法中的低位。

                                              -

                                              这其实就是我们用的列竖式一行一行加起来的一个过程。

                                              -

                                              img

                                              -

                                              S是符号位,GM是乘法标志位。

                                              -

                                              控制门:当最后一位是1时,控制门打开,X中的被乘数进入加法器。

                                              -
                                              -
                                                -
                                              1. 部分积 乘数

                                                -
                                              2. -
                                              3. 乘数不用符号位,写数值位即可

                                                -
                                              4. -
                                              5. 按照是0是1,要么+被乘数要么+0

                                                -
                                              6. -
                                              7. 右移(连符号位一起逻辑右移)

                                                -

                                                image-20230620232152706

                                                -
                                              8. -
                                              9. 直到乘数全部移完

                                                -
                                              10. +

                                                这一点很好理解。因为Document和Element对象的获取元素方法都继承自Node结点,本意就是获取子元素对象。只不过Document是根节点,所以就变成了获取所有元素对象。

                                                +
                                                  +
                                                • getElementById(String id):根据id属性值获取唯一的element对象
                                                • +
                                                • getElementsByTag(String tagName):根据标签名称获取元素对象集合
                                                • +
                                                • getElementsByAttribute(String key):根据属性名称获取元素对象集合
                                                • +
                                                • getElementsByAttributeValue(String key, String value):根据对应的属性名和属性值获取元素对象集合
                                                • +
                                                +
                                                  +
                                                1. 获取属性值
                                                -

                                                Booth算法

                                                  -
                                                1. 部分积 乘数 y补(一开始为0)

                                                  -
                                                2. -
                                                3. 部分积双符号位,乘数单符号位且参与运算

                                                  -

                                                  image-20230620212054291

                                                  -
                                                4. -
                                                5. 每次依据乘数和y补的关系,进行是否加被乘数的决策:

                                                  -

                                                  注意右移不同于原码,是算术右移

                                                  -

                                                  image-20230620212122222

                                                  +
                                                    +
                                                  • String attr(String key):根据属性名称获取属性值
                                                  • +
                                                  +
                                                    +
                                                  1. 获取文本内容
                                                  2. +
                                                  +
                                                    +
                                                  • String text():获取字标签的所有纯文本内容
                                                  • +
                                                  • String html():获取标签体的所有内容(包括字标签的字符串内容)
                                                  • +
                                                6. -
                                                7. 最后一步不用移位

                                                  -

                                                  image-20230620212150280

                                                  +
                                                8. Node:节点对象

                                                  +
                                                    +
                                                  • 是Document和Element的父类
                                                  • +
                                                -

                                                除法运算

                                                逻辑左移

                                                -

                                                最后得到的余数还得乘个2的-n次方

                                                -

                                                恢复余数法

                                                  -
                                                1. 被除数(余数) 商
                                                2. -
                                                3. 先加上 - 除数的补
                                                4. -
                                                5. 如果得到结果≥0,则上商1,左移
                                                6. -
                                                7. 如果小于0,则上商0,+除数补,左移
                                                8. -
                                                9. 左移5次(商包括符号位的所有数字被填满),最后一次上商不用移位
                                                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);
                                                    }
                                                    }

                                                    }
                                                  -

                                                  不恢复余数法(加减交替法)

                                                  -

                                                  image-20230621200001791

                                                  -

                                                  总结一下,大概流程:

                                                  -

                                                  ①准备阶段:MQ清零【存放商】,ACC放入被除数,X放入除数

                                                  -

                                                  ②ACC - X中的值

                                                  -

                                                  ③若ACC中值【上一轮的余数】为负,则上商0;为正,则上商1.ACC左移一位。判断MQ的最后一位【上商的值】,若为负,则ACC + X中的y;为正,ACC - X中的y。【注意,若为第一次减去X,则当余数为正时,就即刻发生溢出错误退出】

                                                  -

                                                  ④重复③,直到移位n次。

                                                  -

                                                  img

                                                  -

                                                  V表示是否溢出。

                                                  -
                                                  +

                                                  第四部分 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. 被除数(余数) 商

                                                      -
                                                    2. -
                                                    3. 先加上 - 除数的补

                                                      -
                                                    4. -
                                                    5. 如果得到结果≥0,则上商1,左移,下一次继续加 - 除数的补

                                                      -
                                                    6. -
                                                    7. 如果小于0,则上商0,左移,下一次加除数的补

                                                      -

                                                      image-20230620234054088

                                                      -

                                                      逻辑左移

                                                      +
                                                    8. 直接将项目放到webapps目录下即可。 * /hello:项目的访问路径–>虚拟目录 * 简化部署:将项目打成一个war包,再将war包放置到webapps目录下。

                                                      +
                                                        +
                                                      • war包会自动解压缩
                                                      • +
                                                    9. -
                                                    10. 左移5次(商包括符号位的所有数字被填满),最后一次上商不用移位

                                                      +
                                                    11. 配置conf/server.xml文件
                                                      <Host>标签体中配置
                                                      <Context docBase="C:\aWorkSpace\Projects\Java\JavaWeb" path="/web" />

                                                    -

                                                    浮点运算

                                                    舍入

                                                    -

                                                    (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

                                                      +
                                                      然后之后访问时输入`localhost/web/JavaWeb.html`即可
                                                      +
                                                      +* docBase:项目存放的路径
                                                      +* path:虚拟目录
                                                      +
                                                      +
                                                        +
                                                      1. 在conf\Catalina\localhost创建任意名称的xml文件。在文件中编写
                                                        <Context docBase="C:\aWorkSpace\Projects\Java\JavaWeb" />
                                                          +
                                                        • 虚拟目录:xml文件的名称
                                                        • +
                                                      -

                                                      寄存器堆

                                                      同步写异步读

                                                      -

                                                      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

                                                      -

                                                      一、指令周期

                                                      +
                                                      注意,该方法是热部署的。也就是说,可以不关闭服务器的情况下,去增删xml文件,会马上变化,而不是像上面两种方式一样重启生效。
                                                      +
                                                      +
                                                    4. +
                                                  +

                                                  动态项目目录结构

                                                  项目都存放在webapp里。打开webapp中的任一个。

                                                  +

                                                  image-20221226221811747

                                                  +

                                                  WEB-INF下是动态资源,也就是Java控制的一些文件【大概这个意思】。有这个文件夹的项目是动态项目。

                                                  +

                                                  WEB-INF以外的都是静态资源。

                                                  +

                                                  image-20221226221933346

                                                  +

                                                  tomcat集成到IDEA

                                                  使用maven创建Web项目
                                                  更换maven镜像源

                                                  idea中Maven镜像源详细配置步骤(对所有项目)

                                                  +
                                                  创造项目

                                                  image-20221226235520519

                                                  +

                                                  然后等着它开始下载就行了。

                                                  +

                                                  最后的目录结构:

                                                  +

                                                  image-20221226235607604

                                                  +

                                                  如果java或者resources目录没有,自己建就行。

                                                  +
                                                  加入tomcat

                                                  1.

                                                  +

                                                  TOMCAT -> IDEA

                                                  +

                                                  2.

                                                  +

                                                  还有另一种更便捷的方式,就是直接添加maven的tomcat插件。在pom.xml文件里加入此段:

                                                  +
                                                  <build>
                                                  <plugins>
                                                  <plugin>
                                                  <groupId>org.apache.tomcat.maven</groupId>
                                                  <artifactId>tomcat7-maven-plugin</artifactId>
                                                  <version>2.2</version>
                                                  </plugin>
                                                  </plugins>
                                                  </build>
                                                  + +

                                                  即可,可用alt+insert自动补全。

                                                  +

                                                  这里我出现了一个飘红报错问题,用这个可以解决:

                                                  +

                                                  maven学习 & Plugin ‘org.apache.tomcat.maven:tomcat7-maven-plugin:2.2’ not found报错解决【问题及解决过程记录】

                                                  +

                                                  然后,右键项目就可以run了:

                                                  +

                                                  image-20221227003805955

                                                  +

                                                  如果没有此选项,就去下载maven helper插件。

                                                  +
                                                  修改tomcat配置参数
                                                  图形化界面

                                                  run-edit configuration-tomcat

                                                  +
                                                  配置文件

                                                  image-20221230221339275

                                                  +

                                                  启动服务器时控制台前几句输出有一句这样的。对应目录下的就可以找到tomcat配置文件。

                                                  +

                                                  Servlet

                                                  server applet运行在服务器端的小程序

                                                  +

                                                  servlet是java编写的服务器端的程序,运行在web服务器中。作用:接收用户端发来的请求,调用其他java程序来处理请求,将处理结果返回到服务器中

                                                  +

                                                  image-20221227154053743

                                                  +

                                                  servlet是接口,定义了Java类被tomcat执行、被浏览器访问的规则。

                                                  +

                                                  image-20221227154222612

                                                  +

                                                  快速入门

                                                  image-20221227154841102

                                                  +

                                                  这里的配置用的是注解,具体原理在第一部分的JavaSE基础里有详细描述了。

                                                  +
                                                  +

                                                  使用maven创建web项目见上面的tomcat-tomcat集成到IDEA-使用maven创建web项目

                                                  +
                                                  +
                                                  +

                                                  如果已经导入依赖坐标却还未生效,就点击右侧侧边栏的maven刷新。

                                                  +

                                                  Maven导入依赖后还不出现Servlet的问题

                                                  +
                                                  +

                                                  原理

                                                  执行原理

                                                    +
                                                  • 执行原理:
                                                      +
                                                    1. 当服务器接受到客户端浏览器的请求后,会解析请求URL路径,获取访问的Servlet的资源路径
                                                    2. +
                                                    3. 查找web.xml文件,是否有对应的标签体内容。
                                                    4. +
                                                    5. 如果有,则在找到对应的全类名【注意:在下面,url-pattern都使用注解配置方法了,所以这两步应该是不用了,应该会变成这样:① 逐个遍历注册的servlet实现类,查看其注解属性是否为对应的url-pattern。② 如果有,则找到类名,步骤继续】
                                                    6. +
                                                    7. tomcat会将字节码文件加载进内存,并且创建其对象
                                                    8. +
                                                    9. 调用其方法
                                                    10. +
                                                    +
                                                  • +
                                                  +

                                                  生命周期

                                                  image-20221227161516964

                                                  +

                                                  并发安全

                                                  Servlet的init方法只执行一次,一种Servlet在内存中只存在一个对象,Servlet是单例的。因而,当多线程同时访问同一个Servlet对象时,就会产生线程安全问题。所以有需要的话,就要采取手段保障Servlet类的线程安全性。

                                                  +

                                                  体系结构

                                                  为了简化开发,我们可以用提供的servlet的实现类。

                                                  +

                                                  image-20221227163713317

                                                  +

                                                  GenericServlet

                                                  除了service方法之外的方法,差不多都只做了空实现。所以只需写service方法即可。

                                                  +
                                                  @WebServlet("/demo2")
                                                  public class Servletdemo2 extends GenericServlet {
                                                  public void service(ServletRequest servletRequest, ServletResponse servletResponse){
                                                  }
                                                  }
                                                  + +

                                                  HttpServlet

                                                  使用

                                                  比如httpservlet,就只用重写里面的doGet和doPost两个方法就行。

                                                  +
                                                  @WebServlet("/demo2")
                                                  public class Servletdemo2 extends HttpServlet {
                                                  @Override
                                                  protected void doGet(HttpServletRequest req, HttpServletResponse resp){
                                                  System.out.println("get!!!");
                                                  }

                                                  @Override
                                                  protected void doPost(HttpServletRequest req, HttpServletResponse resp){
                                                  System.out.println("post!!!");
                                                  }
                                                  }

                                                  + +

                                                  这两个方法的区别就是,当使用get方式提交表单,就会执行第一个方法;使用post则会执行第二个方法。

                                                  +

                                                  比方说post时:

                                                  +

                                                  网页代码如下(放在webapp目录下)

                                                  +
                                                  <!DOCTYPE html>
                                                  <html lang="en">
                                                  <head>
                                                  <meta charset="UTF-8">
                                                  <title>Title</title>
                                                  </head>
                                                  <body>
                                                  Hello,World!
                                                  <!-- action内写Servlet的资源路径 -->
                                                  <form action="/webdemo4_war/demo2" method="post">
                                                  name: <input type="text" name="username" id="username" placeholder="请输入用户名">
                                                  <input type="submit" value="submit">
                                                  </form>
                                                  </body>
                                                  </html>
                                                  + +

                                                  servlet代码同上。

                                                  +

                                                  最终在网页中点击提交

                                                  +

                                                  image-20221230172048250

                                                  +

                                                  会跳转到\demo页面【也即servlet的访问路径】,并且在console打印“post!!!”

                                                  +
                                                  +

                                                  为啥会这样呢?

                                                  +

                                                  之前在讲表单的时候说过,form的action属性代表着提交时这个表单会提交给谁,值为一个URL。所以,这里action的值设置为Servlet的路径,意思就是把表单数据发送给了Servelet,由于使用的是post方式,因此触发了Servlet的doPost方法。Servlet对得到的数据进行各种处理,并且通过req和resp进行交互。

                                                  +
                                                  +
                                                  +

                                                  为什么此处写的是“\demo”这样的路径?

                                                  +

                                                  事实上这是一个相对路径。

                                                  +

                                                  image-20221230215927404

                                                  +

                                                  部署的根路径可以在 run-edit configuration-tomcat-deployment中找到。

                                                  +
                                                  +
                                                  深层一些的问题
                                                  分成get和post

                                                  之所以这两种方法需要分别处理,是因为在Servlet的service方法中,其实是要对req对象进行参数分解,这两种方法分解方式不一样。

                                                  +

                                                  按照以往,我们需要这样写

                                                  +
                                                   public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
                                                  String method = ((HttpServletRequest)servletRequest).getMethod();
                                                  if("GET".equals(method)){
                                                  //执行get的逻辑
                                                  }
                                                  else if ("POST".equals(method)){
                                                  //执行post的逻辑
                                                  }
                                                  }
                                                  + +

                                                  就类似于可以这么写:

                                                  +
                                                  public void service(ServletRequest servletRequest, ServletResponse servletResponse){
                                                  String method = ((HttpServletRequest)servletRequest).getMethod();
                                                  if("GET".equals(method)){
                                                  doGet(servletRequest,servletResponse);
                                                  }
                                                  else if ("POST".equals(method)){
                                                  doPost(servletRequest,servletResponse);
                                                  }
                                                  }
                                                  + +

                                                  于是最后就融合入httpservlet了。

                                                  +

                                                  url-pattern配置

                                                    +
                                                  1. 一个Servlet可以定义多个访问路径 : @WebServlet({“/d4”,”/dd4”,”/ddd4”})

                                                    +
                                                  2. +
                                                  3. 路径定义规则:

                                                      -
                                                    1. 基本概念
                                                    2. +
                                                    3. /xxx:路径匹配如/demo、/*【第一个优先级大于第二个】
                                                    4. +
                                                    5. /xxx/xxx:多层路径,目录结构
                                                    6. +
                                                    7. *.do:扩展名匹配不能在前面加’/‘。也即:
                                                      @WebServlet("*.do")
                                                      + +url访问填写http://localhost/webdemo4_war/*.do
                                                    -

                                                    ① 指令周期

                                                    -

                                                    ② 每条指令的指令周期不同

                                                    -

                                                    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。

                                                    -
                                -

                                流水线处理器

                                流水线概述

                                流水线

                                image-20230618150003983

                                -

                                这点我觉得讲得挺好的。以前只知道流水线通过并行来加速指令执行,但这里给出了一个新的思路:如果是单周期处理器,则RISC-V的时钟周期受执行时间最长的指令限制;如果是流水线处理器,时钟周期就可以由某个步骤决定,主频就可以加快。这个出发点很有意思。

                                -

                                如果流水线各阶段平衡,也即每个阶段需要的执行时间差不多,则

                                -

                                image-20230618150515599

                                -

                                也即在理想条件和有大量指令的情况下,流水线带来的加速比约等于流水线的级数,若各阶段不完全平衡,加速比会变小。

                                -

                                流水线技术是通过提高指令的吞吐率来提高性能的。

                                -

                                RISC-V与流水线

                                我们可以看到,比起X86,RISC-V是面向流水线设计的,其特性与流水线高度相关:

                                +

                                service参数

                                image-20230102005833327

                                +

                                http协议

                                概述

                                概念:Hyper Text Transfer Protocol 超文本传输协议

                                +
                                  +
                                • 传输协议:定义了客户端和服务器端通信时发送数据的格式

                                  +
                                • +
                                • 特点:

                                    -
                                  1. 指令长度相同

                                    -

                                    简化IF和ID

                                    +
                                  2. 基于TCP/IP的高级协议需要先经历三次握手,可靠传输
                                  3. +
                                  4. 默认端口号:80
                                    +

                                    如果说域名是ip地址的简化表示,ip地址又表示着一台主机,那么使用http协议访问一个网址,相当于访问一台主机,并且端口号为80.

                                    +
                                  5. -
                                  6. 只有六种指令格式,格式整齐

                                    -

                                    能在一个阶段内完成译码和读寄存器(ID)

                                    +
                                  7. 基于请求/响应模型的:一次请求对应一次响应
                                  8. +
                                  9. 无状态的:每次请求之间相互独立,不能交互数据
                                  10. +
                                  +

                                  历史版本:

                                  +
                                    +
                                  • 1.0:每一次请求响应都会建立新的连接
                                  • +
                                  • 1.1:复用连接
                                  • +
                                • -
                                • 只通过load、store访存

                                  -

                                  可以利用EX阶段计算存储器地址,然后在下一阶段访存(MEM)

                                  +
                                +
                                报文格式
                                请求

                                客户端发送给服务器端的消息

                                +

                                数据格式:

                                +
                                  +
                                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. 相对安全
                                        -

                                        流水线冒险

                                        image-20230618151040625

                                        -

                                        结构冒险

                                        image-20230618151208189

                                        -

                                        数据冒险

                                        image-20230618151243620

                                        -
                                        解决方法
                                        前递

                                        image-20230618151601721

                                        -
                                        编译重排

                                        image-20230618151706080

                                        -
                                        停顿(气泡)

                                        实在不行只能暂停流水线了

                                        -

                                        image-20230618151637437

                                        -

                                        控制冒险

                                        image-20230619220308876

                                        -
                                        解决方法
                                        硬件支持

                                        image-20230619220259945

                                        -
                                        分支预测
                                          -
                                        1. 遇到分支预测就停顿

                                        2. -
                                        3. 分支预测

                                          +
                                      +
                                    • +
                                    +
                                  • +
                                  +
                                2. +
                                3. 请求头:客户端浏览器告诉服务器一些信息
                                  请求头名称: 请求头值

                                  +
                                    +
                                  • 常见的请求头:

                                      -
                                    1. 静态分支预测

                                      -

                                      image-20230618153106322

                                      +
                                    2. User-Agent:浏览器告诉服务器,我访问你使用的浏览器版本信息 * 可以在服务器端获取该头的信息,解决浏览器的兼容性问题

                                    3. -
                                    4. 动态分支预测

                                      -

                                      image-20230618153125295

                                      +
                                    5. Accept:可以支持的响应格式

                                    6. -
                                    +
                                  • Accept-language:可以支持的语言环境

                                  • +
                                  • Referer:http://localhost/login.html * 告诉服务器,我(当前请求)从哪里来?

                                    +
                                      +
                                    • 作用:
                                    • +
                                    +
                                      +
                                    1. 防盗链:image-20230101235437317如果ref头非合法就不播放
                                    2. +
                                    3. 统计工作:看从哪个网站来的人数多
                                    -

                                    流水线数据通路和控制

                                    流水线数据通路

                                    流水线寄存器

                                    image-20230618154028950

                                    -

                                    image-20230618154608423

                                    -

                                    66666,这个帅

                                    -

                                    image-20230618154815411

                                    -

                                    流水线控制

                                    数据冒险:前递与停顿

                                    前递

                                    分类

                                    前递有两种情况:

                                    -

                                    image-20230618155952018

                                    -
                                    前递产生条件
                                      -
                                    1. RegWrite != 0(写有效)
                                    2. -
                                    3. Rd != x0
                                    4. + +
                                    5. Connection:连接是否活着

                                      +
                                    -
                                    解决方法

                                    流水线寄存器解决:

                                    -

                                    image-20230618160141827

                                    -

                                    并且增加前递所需硬件。

                                    -

                                    停顿

                                    流水线寄存器解决:

                                    -
                                      -
                                    1. 置ID/EX寄存器中控制信号为0(防止寄存器和存储器被写入数据),执行空指令nop

                                    2. -
                                    3. 禁止PC寄存器和IF/ID寄存器内容改变

                                      -

                                      下一条指令就能重新取指

                                      +
                                  +
                                4. +
                                5. 请求空行
                                  空行,就是用于分割POST请求的请求头,和请求体的。

                                  +
                                6. +
                                7. 请求体(正文):

                                  +
                                    +
                                  • 封装POST请求消息的请求参数的
                                  • +
                                -

                                控制冒险

                                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

                                +

                                字符串格式:

                                +
                                //请求行
                                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. 取值周期

                                  -

                                  image-20230621201345807

                                  +
                                2. 响应行

                                  +
                                    +
                                  1. 组成:协议/版本 响应状态码 状态码描述 HTTP/1.1 200 OK

                                  2. -
                                  3. 间址周期

                                    -

                                    image-20230621201351939

                                    +
                                  4. 响应状态码:服务器告诉客户端浏览器本次请求和响应的一个状态, 状态码都是3位数字. 分类:

                                    +
                                      +
                                    1. 1xx:服务器接收客户端消息,但没有接收完成,等待一段时间后,发送1xx多状态码,询问是否还要继续发

                                    2. -
                                    3. 执行周期 ①访存指令 ②非访存指令 ③转移指令 ④三类指令的指令周期

                                      -

                                      image-20230621201358396

                                      -

                                      imgimg

                                      -

                                      image-20230621201423245

                                      +
                                    4. 2xx:成功。代表:200

                                    5. -
                                    6. 中断周期 硬件法和软件法

                                      -

                                      imgimg

                                      -

                                      硬件和软件法。

                                      +
                                    7. 3xx:重定向。代表:302(重定向),304(访问缓存)

                                      +

                                      image-20230103151112791

                                      +

                                      需要自动重定向到另一个C去

                                      +

                                      image-20230103151237984

                                      +

                                      发现资源未变化且本地有缓存

                                    8. -
                                    -

                                    二、控制单元的功能

                                    +
                                  5. 4xx:由客户端造成的错误

                                    +

                                    代表:

                                      -
                                    1. 输入信号

                                      -

                                      ①时钟信号 ②指令寄存器【控制信号与操作码有关】 ③标志 ④外来信号【中断请求、总线请求】

                                      +
                                    2. 404(请求路径没有对应的资源,可能路径输错了)

                                    3. -
                                    4. 输出信号

                                      -

                                      ①CPU内各种控制信号【比如(PC)+1->PC这种】

                                      -

                                      ②送至控制总线的信号【比如中断响应、总线响应】

                                      +
                                    5. 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);
                                      }
                                    6. +
                                  6. -
                                  7. 控制信号举例

                                    -

                                    ①不使用内部总线

                                    -

                                    ②采用内部总线

                                    +
                                  8. 5xx:服务器端错误。

                                    +

                                    代表:500(服务器内部出现Exception)

                                    +
                                    int i = 3/0;
                                  9. +
                                3. -
                                4. 多级时序系统

                                  +
                                +
                              2. +
                              3. 响应头:

                                  -
                                1. 机器周期

                                  -

                                  取指周期=机器周期=最复杂的微操作所需时间【访存】

                                  -

                                  在机器周期内部也需要有时钟来控制微操作的执行顺序

                                  +
                                2. 格式: [头名称 : 值]

                                3. -
                                4. 时钟周期(节拍、状态)

                                  -

                                  每个指令周期都可分为若干个机器周期,每个机器周期都可分为若干个节拍(时钟周期)。一个机器周期内包含多少节拍与需要发送多少控制信号、控制信号复杂度、控制信号能否并行有关。

                                  -

                                  时钟产生节拍信号,不同的节拍信号有不同的先后顺序。

                                  -

                                  一个时钟周期产生一个或几个【并行的几个,或者是操作时间很短,虽然有一定的先后顺序,但可以在一个节拍内完成】微操作命令

                                  -

                                  时钟信号利用上升沿让CU发出控制命令【微操作】控制各个不同部件。

                                  +
                                5. 常见的响应头:

                                  +
                                    +
                                  1. Content-Type:服务器告诉客户端本次响应体 数据格式以及编码格式

                                    +

                                    浏览器依照编码格式来对该页面进行解码。

                                  2. -
                                  +
                                6. Content-disposition:服务器告诉客户端以什么格式打开响应体数据

                                  +
                                    +
                                  • 值:
                                      +
                                    • in-line:默认值,在当前页面内打开
                                    • +
                                    • attachment;filename=xxx:以附件形式打开响应体。也即点击超链接后开始文件下载
                                    • +
                                  • -
                                  • 控制方式

                                    -

                                    ①同步控制方式 采用定长的机器周期、不定长的机器周期、中央控制和局部控制相结合

                                    -

                                    ​ 当指令大多都是可以提前确定的,就用同步。当一条微操作的时间很难控制,可以采用异步控制。

                                    -

                                    ②异步控制方式 等待IO读写

                                    -

                                    ③联合控制方式 同步与异步结合

                                    -

                                    ④人工控制

                                    +
                                -

                                三、组合逻辑设计

                                -
                                  -
                                1. 组合逻辑控制单元框图

                                  -

                                  ①CU外特性 ②节拍信号

                                2. -
                                3. 微操作的节拍安排

                                  -

                                  ①安排微操作时序的原则

                                  -

                                  原则一:先后顺序不更改。

                                  -

                                  原则二:可以并行执行的,且微操作间没有先后顺序的,就尽量把它们安排在一个节拍中。

                                  -

                                  原则三:时间较短微操作尽量在一个节拍内且可以有先后顺序。

                                  -

                                  ②取值周期间址周期执行周期的

                                  -

                                  image-20230621201709245

                                  -

                                  image-20230621201717225

                                  -

                                  image-20230621201722561

                                  -

                                  image-20230621201732549

                                  +
                                +
                              4. +
                              5. 响应空行

                                +
                              6. +
                              7. 响应体:传输的数据

                              -
                              -

                              存储器

                              概述

                              分类

                              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

                              +

                              字符串格式:

                              +
                              //响应行
                              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();
                                +
                                -

                                img

                                -

                                14位的地址分了两次传,分别作为行列地址。

                                -

                                RAS:行选控制信号 CAS:列选控制信号 WE:读写控制信号。产生的时钟控制了芯片内部的读写操作

                                -

                                img

                                -

                                如果读放大器左边有电,那么右边输出没电;左没电右有电.这样,读放大器左边的部分,有电表示0,没电表示1 ;读放大器右边的部分,有电表示1,没电表示0.

                                +

                                URL:统一资源定位符 : http://localhost/day14/demo1 中华人民共和国
                                URI:统一资源标识符 : /day14/demo1 共和国

                                +

                                URI的代表范围更大

                                -
                                刷新

                                为什么要刷新:

                                -

                                image-20230619224003589

                                -
                                集中刷新

                                image-20230618221439701

                                -
                                分散刷新

                                image-20230618221603727

                                -
                                异步刷新

                                image-20230618222049113

                                -

                                ROM 只读存储器

                                  -
                                1. 掩膜ROM(MROM) 用户不能修改

                                  -

                                  image-20230618222716561

                                2. -
                                3. PROM(一次性编程) 破坏性编程

                                  -

                                  image-20230618223116398

                                  +
                                4. 获取协议及版本 HTTP/1.1

                                  +
                                  String getProtocol()
                                5. +
                                6. 获取访问的客户机的IP地址

                                  +
                                  String getRemoteAddr()
                                7. +
                                +
                                获取请求头
                                  +
                                1. 通过请求头的名称获取请求头的值

                                  +
                                  String getHeader(String name)
                                2. +
                                3. 获取所有的请求头名称

                                  +
                                  Enumeration<String> getHeaderNames()
                                  + +

                                  返回的是一个迭代器

                                4. -
                                5. EPROM(多次性编程)

                                  -

                                  image-20230618223151914

                                  +
                                +
                                public class Servletdemo2 extends GenericServlet {

                                public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
                                HttpServletRequest req = (HttpServletRequest) servletRequest;
                                Enumeration<String> enumerator = req.getHeaderNames();
                                while(enumerator.hasMoreElements()){
                                String name = enumerator.nextElement();
                                System.out.println(name);
                                //System.out.println(name+"-----"+req.getHeader(name));
                                }
                                }
                                }

                                输出结果:
                                host
                                connection
                                sec-ch-ua
                                sec-ch-ua-mobile
                                sec-ch-ua-platform
                                upgrade-insecure-requests
                                user-agent
                                accept
                                purpose
                                sec-fetch-site
                                sec-fetch-mode
                                sec-fetch-user
                                sec-fetch-dest
                                accept-encoding
                                accept-language
                                cookie
                                + +

                                这些请求头名称正是上面的键值对里的键。

                                +
                                获取请求体

                                request将请求体中的数据封装成了流。如果数据是字符,那就是字符流;是视频这种的字节,那就是字节流。

                                +
                                * 步骤:
                                +    1. 获取流对象
                                +  *  BufferedReader getReader():获取字符输入流,只能操作字符数据
                                +  *  ServletInputStream getInputStream():获取字节输入流,可以操作所有类型数据
                                +    2. 操作流获取数据
                                +
                                +
                                @WebServlet("/demo2")
                                public class Servletdemo2 extends GenericServlet {

                                public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
                                HttpServletRequest req = (HttpServletRequest) servletRequest;
                                BufferedReader bfr = req.getReader();
                                String line;
                                while((line = bfr.readLine())!=null){
                                System.out.print(line);
                                }
                                }
                                }
                                + +

                                请求体中键值对会在一行里,用&分割

                                +
                                +

                                获取时中文乱码

                                +
                                  +
                                • get方式:tomcat 8 已经将get方式乱码问题解决了

                                • -
                                • EEPROM(电可擦写)

                                  -

                                  image-20230618223229585

                                  +
                                • post方式:会乱码

                                  +
                                                     * 解决:在获取参数前,设置流的编码:
                                  +
                                  +    
                                  request.setCharacterEncoding("utf-8");
                                  +
                                • -
                                • Flash Memory(闪速型存储器)

                                  -

                                  image-20230618223252821

                                  +
                                +
                                +
                                获取请求参数通用的方法(通用指对get和post通用)

                                这里的请求参数应该是指上面Post的请求体、Get的请求行里的参数,请求头里的参数是获取不到的。

                                +
                                  +
                                1. 根据参数名称获取参数值

                                  +
                                  String getParameter(String name)
                                  + +

                                  如 username=zs&password=123,getParameter(“username”)会得到zs。

                                2. -
                                -

                                存储器与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指令给出的参数以及要求的操作,会编写一个由通道指令组成的通道程序,并且会把程序放到内存或者是通道内存的指定位置,之后启动通道进行工作。

                                +
                              10. 根据参数名称获取参数值的数组

                                +
                                String[] getParameterValues(String name)
                                + +

                                如 hobby=xx&hobby=game,会得到{xx,game}

                                +
                              11. +
                              12. 获取所有请求的参数名称

                                +
                                Enumeration<String> getParameterNames()
                              13. +
                              14. 取所有参数的map集合

                                +
                                Map<String,String[]> getParameterMap()
                              15. +
                              +
                              请求转发

                              在服务器内部资源跳转

                              +

                              image-20230102195615676

                              +

                              AServlet做了一部分事情,把剩余的事情交给BServlet去做

                              +

                              步骤:

                              +
                                +
                              1. 通过request对象获取请求转发器对象

                                +
                                RequestDispatcher getRequestDispatcher(String path)
                              2. +
                              3. 使用RequestDispatcher对象来进行转发

                                +
                                requestDispatcher.forward(ServletRequest request, ServletResponse response) 
                              4. +
                              +
                              @WebServlet("/demo2")
                              public class Servletdemo2 extends GenericServlet {

                              public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
                              System.out.println("I am "+Servletdemo2.class.getName());
                              //进行转发
                              servletRequest.getRequestDispatcher("/demo3")
                              .forward(servletRequest,servletResponse);
                              }
                              }

                              @WebServlet("/demo3")
                              public class ServletDemo3 extends GenericServlet {

                              public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
                              System.out.println("I am "+ServletDemo3.class.getName());
                              }
                              }
                              + +

                              特点:

                              +
                                +
                              1. 浏览器地址栏路径不变
                              2. +
                              3. 只能在服务器内部跳转,只能转发到服务器内部的资源中
                              4. +
                              5. 转发是一次请求,多个资源使用的是同一次请求
                              6. +
                              +
                              共享数据

                              接力工作的两个Servlet可以通过request对象进行数据通信。

                              +
                              * 方法:
                              +
                              +
                                +
                              1. 存储键值对

                                +
                                void setAttribute(String name,Object obj)
                              2. +
                              3. 获取值

                                +
                                Object getAttitude(String name)
                              4. +
                              5. 移除键值对

                                +
                                void removeAttribute(String name)
                              6. +
                              +
                              获取ServletContext
                              ServletContext getServletContext()
                              + +
                              练习:结合数据库与Servlet进行用户登录
                              +

                              要求:

                              +

                              1.编写login.html登录页面
                              username & password 两个输入框
                              2.使用Druid数据库连接池技术,操作mysql,day14数据库中user表
                              3.使用JdbcTemplate技术封装JDBC
                              4.登录成功跳转到SuccessServlet展示:登录成功!用户名,欢迎您
                              5.登录失败跳转到FailServlet展示:登录失败,用户名或密码错误

                              -

                              连接方式

                              编址

                              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

                              +
                              文件结构

                              ![屏幕截图 2023-01-02 235207](./JavaWeb/屏幕截图 2023-01-02 235207.png)

                              -

                              以下介绍的是链式排队器

                              +

                              错误历程

                                -
                              1. INTR默认为0,取非为1. 经&后整个排队电路为1
                              2. -
                              3. 当i设备发出请求,INTRi=1,取非为0,经&后变为0,INTPi之后的电路清零,只有i之前的INTP为1
                              4. -
                              5. 3在一连串的显示为 1 的INTP中,最后一个显示1的设备优先级最高。因为按照我们的分析,是它发出了请求
                              6. +
                              7. lib目录位置错误

                                +

                                NoClassDefFoundError解决方案一开始lib目录没放进web-inf,通过此文章得知错误为包未引入,再由下面这篇文章得知lib目录放置错误

                                +

                                JDBC Template报错:java.lang.ClassNotFoundException: org.springframework.jdbc.core.RowMapper

                                +
                              8. +
                              9. 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……放在下面果然就好了。

                                +
                              -

                              使用与非+非而不是直接与门是因为与非门+非更便宜。

                              -

                              我猜这个意思是,链式排队的话,越前面的优先级越高,现在我们讲的是怎么快速****找出****最高的最前面的是哪一个。之所以为什么越前面的优先级最高,可从这个电路中得知。如果一个东西发出请求,那么它后面的INTPi’都会被置零,因而它肯定比它后面的高级。因此越前面的优先级越高。

                              -

                              https://www.likecs.com/show-390301.html

                              -

                              img

                              -

                              这个可以验证我的观点。至于这个轮询方式,应该在第三章的总线那边讲过,应该用的是链式查询。

                              -
                              软件实现

                              程序查询

                              -

                              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

                              -]]>
                              -
                              - - 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;
                              }
                              +
                              druid.properties
                              driverClassName=com.mysql.jdbc.Driver
                              url=jdbc:mysql://localhost:3306/helloworld
                              username=root
                              password=root
                              initialSize=5
                              maxActive=10
                              maxWait=3000
                              -

                              也就是说我能过测试是因为偶然吗??

                              -

                              我其实感觉正确理解的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) ;
                              +
                              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>
                              -

                              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; }
                              };
                              +
                              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("登录失败,用户名或密码错误");
                              }
                              }
                              -

                              集合

                              我们需要维护一个左端点升序的区间集合,故使用内部红黑树实现的有序集合set。

                              -
                              set<node> buffer;   // 存储结构
                              +
                              @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);
                              }

                              }
                              }
                              -

                              算法

                              我们要做的,是对数据包进行整流,并且把整流过的部分输送到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中。

                              +

                              关于BeanUtils

                              +

                              BeanUtils工具类,简化数据封装, 用于封装JavaBean的

                                -
                              1. buffer进行第一轮扫描

                                -

                                如果node与buffer[i]产生重叠((left >= it->left && left <= it->right)),那么更新node为node∪buffer[i],并且将buffer[i]从buffer中删去。

                                -

                                在第一次找到重叠的区间,就应该break退出第一轮循环。

                                +
                              2. JavaBean:标准的Java类

                                +
                                 1. 要求:
                                +     1. 类必须被public修饰
                                +     2. 必须提供空参的构造器
                                +     3. 成员变量必须使用private修饰
                                +     4. 提供公共setter和getter方法
                                + 2. 功能:封装数据
                                +
                              3. -
                              4. buffer进行第二轮扫描

                                -

                                如果node与buffer[i]产生重叠( (it->left >= left && it->left <= right)),那么更新node为node∪buffer[i],并且将buffer[i]从buffer中删去。

                                +
                              5. 概念:

                                +

                                ​ 成员变量:
                                ​ 属性:setter和getter方法截取后的产物

                                +
                                           例如:getUsername() --> Username--> username
                                +
                                +
                              6. +
                              7. 方法:

                                +
                                1. setProperty()
                                +1. getProperty()
                                +1. populate(Object obj , Map map):
                                +
                                +

                                ​ 将map集合的键值对信息,封装到对应的JavaBean对象中

                              -

                              我们在合并区间时,不仅需要对struct node的左端点left和右端点right进行更新,还需要对其数据域data也进行合并拼接。我们维护变量res作为维护的目标区间的数据域。对于res,我们应该进行如下操作:

                              +
                              +
                              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. 初始化为data

                                -
                              2. -
                              3. 除去[left, left_bound)这一区间内的数据

                                -

                                这部分数据我们已经整流过并且写入ByteStream

                                +
                              4. 获取输出流

                                +
                                  +
                                1. 字节输出流

                                  +
                                  ServletOutputStream getOutputStream()
                                2. +
                                3. 字符输出流

                                  +
                                  PrintWriter getWriter()
                                4. +
                              5. -
                              6. 在两轮合并中对其进行正确拼接

                                +
                              7. 使用输出流,将数据输出到客户端浏览器

                              -
                              图解

                              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);
                              +
                              案例
                              重定向

                              资源跳转的一种方式。

                              +

                              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);
                              }
                              }
                              -

                              写入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);
                              }
                              }
                              +
                              @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);
                              }
                              }
                              -

                              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;
                              }
                              +
                              输出:
                              I am demo1 1675674230
                              I am demo2 1675674230
                              -

                              但是这样很明显有两个问题。

                              -

                              一是就算你超过了,你也不能直接丢弃掉data,得把没超过的部分填满。

                              -

                              二是,data.length() + unassemble_bytes()有时,甚至是很多时候,都不会是将data并入buffer之后buffer的容量。因为data和buffer很大概率会存在重叠区间。

                              -

                              那么,你能不能在区间合并完之后,再进行该不变性条件的判断,并且将没超过的部分填满,超过的部分丢弃呢?

                              -

                              答案是,也不能。因为经过两轮合并,你的data和buffer里原有的数据早已你中有我我中有你了,你无法在最后将它们分开,找出data超过capacity的数据并且丢弃它。

                              -

                              因而,头尾都不行的话,唯一的答案就是,我们只能在两轮区间合并中途,去时刻追踪当前容量是否超过capacity

                              -

                              这听起来就令人十分地头大。但事实证明,并不是无法实现的,坚持下去,就算是shit山也能跑起来()下面便是我的实现思路。

                              -
                              思路

                              维护一个变量remaining,表示当前还有多少容量。维护start,表示未判断是否可以写入buffer的数据起点。我们要做的事:

                              +

                              重定向的这几行代码其实是可以简化的:

                              +
                              /* 重定向 */
                              //设置状态码
                              resp.setStatus(302);
                              //要填的是完整资源路径。
                              resp.setHeader("location","/practice_war/demo2");
                              + +

                              可以简化为:

                              +
                              resp.sendRedirect("/practice_war/demo2");
                              + +
                              +

                              关于req对象不一样,但hashcode值相同的解释:

                              +

                              hashcode很大程度与对象内存空间相关,与对象的具体内容没什么关系。两个对象拥有相同的hashcode有可能只是因为存储的内存空间位置大小都相同导致的。所以是因为两次的req对象都占用了同一个内存空间【JVM调度问题】,所以才让hashcode值相同。这两个对象实质上是不一样的。

                              +
                              +

                              重定向的特点(与请求转发完全相反):

                                -
                              1. 初始化remaining为capacity - 当前容量,start为掐头后的left
                              2. -
                              3. 在第一轮循环中更新start
                              4. -
                              5. 在第二轮循环中通过start和remaining来判断是否能够写入buffer。尽可能多地写入,把写入不了的部分丢弃。
                              6. +
                              7. 浏览器地址栏路径改变
                              8. +
                              9. 可以访问其他站点的资源
                              10. +
                              11. 使用多次请求,不能使用request对象共享数据
                              -
                              例子

                              下面举个例子来说明整个流程。

                              -
                              initial:  
                              buffer = { [1,3], [5,8],[10,12],[13,14] } , capacity = 12, remaining = 12-9=3, data = [2,11], start = 2
                              +

                              路径写法:

                              +
                                +
                              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");
                                    -

                                    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
                                    +

                                    这个路径将来是给客户端将来要使用的路径,是客户端路径,所以要加虚拟目录。

                                    + +
                                  • +
                                  • 服务器使用:不需要加虚拟目录

                                    +

                                    比如说之前的请求转发

                                    +
                                      +
                                    • 转发路径
                                    • +
                                    +
                                  • +
                                  +
                                • +
                                +
                              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);
                              }
                              }
                              -

                              在循环结束的这里已经处理:

                              -
                              if (remaining < right - start) {
                              right = remaining + start;
                              if (eof == 1)
                              is_eof = false;
                              }
                              +
                              服务器输出字节数据到浏览器
                              @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);
                              }
                              }
                              -

                              */

                              -

                              然后扫描到[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
                              // ...
                              }
                              +
                              验证码
                              @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);
                              }
                              }
                              -

                              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;
                              // ...
                              }
                              }
                              +
                              <!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>
                              -
                              小细节

                              因而,在一开始发现remaining满的时候,不能直接goto end。

                              -
                              // if(remaining == 0)	goto end; // buffer满
                              +

                              ServletContext对象

                              概念

                              代表整个web应用,可以和servlet容器(服务器)通信

                              +

                              获取

                              通过request对象获取
                              ServletContext getServletContext()
                              -

                              因为还得看看ByteStream能不能腾空间。

                              -

                              eof

                              对于eof的处理也是需要注意的一个小细节。

                              -

                              我们不能这么写:

                              -
                              if (eof)
                              _output.end_input();
                              +
                              通过HttpServlet获取
                              this.getContext();
                              -

                              原因有二,一是最后一个数据包有可能是部分丢失,二是整流可能还未结束。

                              -

                              所以我们应该在成员变量中维护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();
                              }
                              }
                              +

                              功能

                              获取MIME类型

                              MIME是在互联网通信过程中定义的一种文件数据类型

                              +
                                      * 格式: 大类型/小类型   text/html        image/jpeg
                              +
                              +
                              /*
                              @param: 文件的后缀扩展名,如.txt
                              */
                              String getMimeType(String file);
                              -

                              代码

                              头文件声明

                              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; }
                              }
                              +

                              image-20230107010006874

                              +

                              mime映射存在了服务器的xml文件中。

                              +

                              使用案例:

                              +
                              System.out.println(this.getServletContext().getMimeType("a.txt"));
                              -

                              具体实现

                              #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; }
                              -]]>
                              -
                              - - 密码学基础 - /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

                          + +
                          共享数据

                          ServletContext是一个域对象,可以用来共享数据。

                          +

                          ServletContext代表着服务器,因而它的生命周期跟随服务器关闭而灭亡。ServletContext可以共享所有请求的数据。也就是说,任何一次请求,任何用户,看到的ServletContext域都是同一个。

                          +

                          这样大的效果也使得我们需要更加谨慎地使用它。一旦数据存入ServletContext域,就只会在服务器关闭后才会消亡,很耗内存。

                          +
                          获取文件的真实(服务器)路径
                          String getRealPath();
                          + +

                          经测试发现,这东西只是起了一个字符串拼接的作用,是不会帮忙检查文件是否存在的。

                          +

                          学到这我顺便看了看文件放在不同的地方最后应该如何访问:

                          +

                          image-20230107012903495

                          +

                          这是最终部署项目文件夹的结构:

                          +

                          image-20230107013010276

                          +

                          可以看到只有bcd被保留了。它们的目录要这样获取:

                          +
                              @Override
                          protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                          ServletContext context = this.getServletContext();

                          System.out.println(context.getRealPath("/WEB-INF/classes/b.txt"));
                          System.out.println(context.getRealPath("/c.txt"));
                          System.out.println(context.getRealPath("/WEB-INF/d.txt"));
                          }
                          /*输出结果:
                          D:\aWorkStorage\etc\apache-tomcat-8.5.83\webapps\practice_war\WEB-INF\classes\b.txt
                          D:\aWorkStorage\etc\apache-tomcat-8.5.83\webapps\practice_war\c.txt
                          D:\aWorkStorage\etc\apache-tomcat-8.5.83\webapps\practice_war\WEB-INF\d.txt
                          是我的电脑里tomcat的目录
                          */
                          + +

                          案例:文件下载

                          要求
                            +
                          • 文件下载需求:
                              +
                            1. 页面显示超链接
                            2. +
                            3. 点击超链接后弹出下载提示框
                            4. +
                            5. 完成图片文件下载,那种会存到你电脑download目录下,而不是直接加载出来的
                            6. +
                            +
                          • +
                          +

                          image-20230201170701909

                          +

                          用户点击下载->请求发送给某个servlet,servlet修改response->tomcat响应用户,传递的图片资源按照response的方法打开

                          +
                          代码

                          说实话看了感觉有点难以下手,主要还是完全不知道html和servlet怎么交互造成的,看了老师讲解才有点恍然大悟。

                          +

                          我们可以把a标签以重定向的角度去看。它会新建一个request,然后发送到它的href中的那个url。在此处我们将url设置为/practice_war/download?filename=1.jpg,也即要以GET的方式发送给download,请求体为filename=1.jpg。然后servlet执行结束后,就会将信息存储在resp中返回给tomcat,由tomcat发送给用户。

                          +
                          html
                          <body>
                          <a href="/practice_war/download?filename=1.jpg" id = "a">点击下载图片</a>
                          </body>
                          + +
                          servlet

                          思路:

                          +

                          获取要下载的资源,并且将其输入到resp的stream中。

                          +

                          有一点需要非常注意:

                          +
                          resp.setContentType(this.getServletContext().getMimeType(path));
                          resp.setHeader("content-disposition","attachment;filename="+name);
                          + +

                          必须要在把资源输入到resp的stream前设置好,精确来说是调用sos.write前设置好,不然无法起作用。

                          +

                          猜测是因为可能resp会根据disposition方式的不同而自动决策write的方式。

                          +
                          @Override
                          protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                          //获取要下载的资源名称
                          String name = req.getParameter("filename");

                          //获取路径
                          String path = this.getServletContext().getRealPath("/img/"+name);
                          //使用字节流
                          FileInputStream fis = new FileInputStream(path);
                          //输出数据

                          resp.setContentType(this.getServletContext().getMimeType(path));
                          resp.setHeader("content-disposition",
                          "attachment;filename="+
                          // 为了防止中文乱码,需要针对不同的浏览器来进行编码
                          DownLoadUtils.getFileName(req.getHeader("user-agent"),name));

                          //获取字节输出流
                          ServletOutputStream sos = resp.getOutputStream();
                          byte[] buff = new byte[1024];
                          int len = 0;
                          while((len = fis.read(buff))!=-1){
                          sos.write(buff,0,len);
                          }
                          //释放资源
                          fis.close();

                          // resp.setContentType(this.getServletContext().getMimeType(path));
                          // resp.setHeader("content-disposition","attachment;filename="+name);
                          }
                          + +

                          会话

                          会话:一次会话中包含多次请求和响应。

                          +
                            +
                          • 一次会话:浏览器第一次给服务器资源发送请求,会话建立,直到有一方断开为止

                            +
                          • +
                          • 功能:请求之间本来是相互独立的。将多次请求组织在一次会话中,就可以让请求之间进行数据的共享。

                            +
                          • +
                          • 方式:

                            +
                              +
                            • 客户端会话技术 Cookie

                              +

                              把数据存进客户端

                              +
                            • +
                            • 服务器端会话技术 Session

                              +

                              把数据存进服务器端

                              +
                            • +
                            +
                          • +
                          +

                          概念

                          客户端会话技术,将数据保存到客户端

                          +

                          快速入门

                            +
                          • 使用步骤:

                              -
                            1. 分组密码一般指对称分组密码
                            2. +
                            3. 创建Cookie对象,绑定数据【为了从服务器端发送cookie给客户端】
                                +
                              • new Cookie(String name, String value)
                              • +
                              • 可以看到,Cookie其实就是一种name-value这样的键值对对象
                              • +
                              +
                            4. +
                            5. 发送Cookie对象【因为要发送给客户端,所以应该在response里存】
                                +
                              • response.addCookie(Cookie cookie)
                              • +
                              +
                            6. +
                            7. 获取Cookie,拿到数据【因为是来自客户端,所以要从request里要】
                                +
                              • Cookie[] request.getCookies()
                              • +
                              +
                            -

                            image

                            +
                          • +
                          • 代码

                            +
                            @WebServlet("/demo")
                            public class ServletDemo extends HttpServlet {
                            @Override
                            protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                            response.addCookie(new Cookie("password","abc123"));
                            }

                            @Override
                            protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                            this.doGet(request, response);
                            }
                            }
                            + +
                            @WebServlet("/demo2")
                            public class ServletDemo2 extends HttpServlet {
                            @Override
                            protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                            System.out.println(request.getCookies());
                            }

                            @Override
                            protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                            this.doGet(request, response);
                            }
                            }
                          • +
                          • 得到效果

                            +

                            运行服务器,首先访问/demo,然后在同一个浏览器再次访问/demo2,就可以在控制台看到输出。

                            +

                            这个过程发生了什么呢?

                            +

                            首先,访问/demo就相当于建立了会话。/demo的Servlet获取到请求之后,在response中将cookie填入。

                            +

                            保持浏览器窗口不变,会话也不变。

                            +

                            再次访问/demo2,cookie信息自动保存在request对象中。/demo2的Servlet获取到请求之后,在控制台中打印输出了cookie。

                            +
                          • +
                          +

                          细节学习

                          一次发送多个cookie

                          你看它那个API叫add,就知道数据结构差不多是个list,所以多次add就行。

                          +
                          保存时间

                          默认情况下,浏览器关闭则cookie就马上被销毁。

                          +

                          如果需要持久化存储:

                          +
                          cookie.setMaxAge(int seconds)
                          + +

                          参数:

                            -
                          1. 明文经编码表示后变成二进制序列
                          2. -
                          3. 二进制序列固定长度分组
                          4. -
                          5. 每组在密钥控制下转为密文分组
                          6. -
                          7. 本质上是明文到密文的一一映射
                          8. -
                          9. 一般明文长度=密文长度,密钥长度不一定
                          10. +
                          11. 正数:将Cookie数据写到硬盘的文件中。持久化存储。并指定cookie存活时间,时间到后,cookie文件自动失效
                          12. +
                          13. 负数:默认值
                          14. +
                          15. 零:删除cookie信息
                          -

                          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

                          +
                          中文问题

                          在tomcat 8 之前 cookie中不能直接存储中文数据,需要将中文数据转码——一般采用URL编码(%E3)

                          +

                          在tomcat 8 之后,cookie支持中文数据。

                          +
                          获取范围
                            +
                          1. 假设在一个tomcat服务器中,部署了多个web项目,那么在这些web项目中cookie能不能共享?

                            +
                              +
                            • 默认情况下cookie不能共享

                              +
                            • +
                            • 共享方法:

                              +

                              setPath(String path):设置cookie的获取范围。默认情况下,设置当前的虚拟目录

                              +

                              如果要共享,则可以将path设置为”/“

                              +
                            • +
                            +
                          2. +
                          3. 不同的tomcat服务器间cookie共享问题?

                            +

                            比如说:

                            +image-20230221225514567 + +
                              +
                            • setDomain(String path):如果设置一级域名相同,那么多个服务器之间cookie可以共享

                              +

                              setDomain(".baidu.com"),那么tieba.baidu.com和news.baidu.com中cookie可以共享)

                              +
                            • +
                            +
                          4. +
                          +

                          作用和特点

                          特点:

                          +
                            +
                          1. cookie存储数据在客户端浏览器

                            +

                            因而它相对不安全

                            +
                          2. +
                          3. 浏览器对于单个cookie 的大小有限制(4kb) 以及 对同一个域名下的总cookie数量也有限制(20个)

                            +
                          4. +
                          +

                          作用:

                          +
                            +
                          1. cookie一般用于存出少量的不太敏感的数据

                            +
                          2. +
                          3. 在不登录的情况下,完成服务器对客户端的身份识别

                            +

                            比如说,以不登录情况下对某个网页进行属性设置,你下次打开的时候属性设置依然在,这是因为你的属性设置的cookie在设置后被存入到你的电脑中,下次访问该网页发出请求,服务器端就能根据请求中cookie里的属性设置信息来做出响应了。

                            +
                          4. +
                          +

                          案例:记住上一次访问时间

                          需求:
                          1. 访问一个Servlet,如果是第一次访问,则提示:您好,欢迎您首次访问。
                          2. 如果不是第一次访问,则提示:欢迎回来,您上次访问时间为:显示时间字符串

                          +
                          public class ServletDemo extends HttpServlet {
                          @Override
                          protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                          //获取流对象之前,设置流的默认编码:ISO-8859-1 设置为:GBK
                          response.setCharacterEncoding("utf-8");

                          //告诉浏览器,服务器发送的消息体数据的编码。建议浏览器使用该编码解码
                          response.setHeader("content-type","text/html;charset=utf-8");
                          if(request.getCookies() != null)
                          for(Cookie c : request.getCookies()){
                          if(c.getName().equals("isfirst")){
                          response.getWriter()
                          .write("<h1>欢迎回来,您上次访问的时间为<h1>"+c.getValue());
                          break;
                          }
                          }
                          else
                          response.getWriter().write("<h1>你好!欢迎你!<h1>");

                          SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");
                          Date date1 = new Date();
                          String currentTime = dateFormat.format(date1);

                          response.addCookie(new Cookie("isfirst",currentTime));
                          }

                          @Override
                          protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                          this.doGet(request, response);
                          }
                          }
                          + +

                          Session

                          概念

                          服务器端会话技术,在一次会话的多次请求间共享数据,将数据保存在服务器端的对象中。HttpSession

                          +

                          应用场合

                          比如说购物网站的购物车这种,就会存在session。想想也是(

                          +
                            +
                          1. session用于存储一次会话的多次请求的数据,存在服务器端

                            +

                            比如说,当我们做重定向的时候,就可以选择用session共享数据(会话域)而非使用ServletContext(此范围过大)

                            +
                          2. +
                          3. session可以存储任意类型,任意大小的数据

                            +
                          4. +
                          5. session与Cookie的区别:

                            +
                              +
                            1. session存储数据在服务器端,Cookie在客户端
                            2. +
                            3. session没有数据大小限制,Cookie有
                            4. +
                            5. session数据安全,Cookie相对于不安全
                            6. +
                            +
                          6. +
                          +

                          快速入门

                            +
                          1. 获取HttpSession对象:
                            HttpSession session = request.getSession();

                            +
                          2. +
                          3. 使用HttpSession对象:

                            +
                              Object getAttribute(String name)  
                            void setAttribute(String name, Object value)
                            void removeAttribute(String name)

                            #### 原理

                            ![image-20230223102722575](./JavaWeb/image-20230223102722575.png)

                            实现依赖于Cookie

                            #### 细节

                            前面说到,当客户端和服务器端有任何一端关闭之后,会话结束,在这种情况下,session在客户端和服务器端的保留情况不同。

                            1. 当客户端关闭后,服务器不关闭,两次获取session是否为同一个?
                            * 默认情况下。不是。
                            * 如果需要相同,则可以创建Cookie,键为JSESSIONID,设置最大存活时间,让cookie持久化保存。
                            ```java
                            Cookie c = new Cookie("JSESSIONID",session.getId());
                            c.setMaxAge(60*60);
                            response.addCookie(c);
                          4. +
                          5. 客户端不关闭,服务器关闭后,两次获取的session是同一个吗?

                            +
                          6. +
                          +
                            +
                          • 不是同一个,但是要确保数据不丢失。tomcat自动(IDEA不会活化)完成以下工作
                              +
                            • session的钝化:(序列化)
                                  * 在服务器正常关闭之前,将session对象序列化到硬盘上
                              +
                              +
                            • +
                            +
                          • +
                          +
                                  * 具体是会放在这里:
                          +
                          +      ![image-20230223104447097](./JavaWeb/image-20230223104447097.png)
                          +    
                          +* session的活化:(反序列化
                          +    * 在服务器启动后,将session文件转化为内存中的session对象即可。
                          +
                          +我想,cookie应该在这点上不会像session这么做,因为cookie本质上是保存在客户端的数据,按理来说服务器端把cookie发出去之后就可以销毁了,在服务器序列化一点意义都没有。
                          +
                          +
                            +
                          1. 销毁时间

                            +
                              +
                            1. 服务器关闭

                              +
                            2. +
                            3. session对象调用invalidate() 。

                              +
                            4. +
                            5. session默认失效时间 30分钟
                              选择性配置修改

                              +

                              可以在每个项目的子配置文件(如下图)或者总的项目的父配置文件apache-tomcat-8.5.83\conf\web.xml中配置

                              +

                              image-20230223105053170

                              +
                              <session-config>
                              <session-timeout>30</session-timeout>
                              </session-config>
                            6. +
                            +
                          2. +
                          +

                          案例

                          +

                          需求:

                          +
                            +
                          1. 访问带有验证码的登录页面login.jsp
                          2. +
                          3. 用户输入用户名,密码以及验证码。
                              +
                            • 如果用户名和密码输入有误,跳转登录页面,提示:用户名或密码错误
                            • +
                            • 如果验证码输入有误,跳转登录页面,提示:验证码错误
                            • +
                            • 如果全部输入正确,则跳转到主页success.jsp,显示:用户名,欢迎您
                            • +
                            +
                          4. +
                          +
                          +
                          初见思路

                          我们可以在服务器端使用session存储password和username的map,存储验证码图片编号和图片的map,然后用cookie携带验证码图片编号,在req中存储用户名和密码。

                          +
                          正确思路

                          感觉我上面的思路是没有充分利用到session的性质,仅仅把它作为在服务器端存储数据的工具,

                          +

                          “在服务器端存储password和username的map,存储验证码图片编号和图片的map,然后用cookie携带验证码图片编号,在req中存储用户名和密码。”

                          +

                          这样也依然成立,跟session没半毛钱关系。我们可以这样使用session:

                          +
                            +
                          1. 在服务器端存储password和username的map,存储验证码图片编号和图片的map
                          2. +
                          3. 当会话建立,由于没有cookie,故而session第一次创建。我们在session内写入验证码对应的编号,把图片通过response发送给客户端。
                          4. +
                          5. 会话端输入图片验证码后,按下submit按键,验证码存入request域,向服务器端发送请求
                          6. +
                          7. 服务器端Servlet从请求中get到验证码,然后在session中get到当前验证码的图片编号,向一开始存储的map查询数据,这样就能验证验证码是否正确了
                          8. +
                          +

                          那么在这里为什么不用Cookie而使用session呢?大概是因为cookie不安全罢(慌乱)

                          +
                          代码
                          jsp
                          <html lang="en">
                          <head>
                          <meta charset="UTF-8">
                          <title>Title</title>
                          </head>
                          <body>
                          <form action="/practice_war/loginServlet" method="post">
                          name: <input type="text" name="username" id="username" placeholder="请输入用户名">
                          password: <input type="password" name="password" id="password" placeholder="请输入密码">
                          verifycode:<input type="text" name="verifycode" id="verifycode" placeholder="请输入验证码">

                          <img id="img" src="/practice_war/check"/>

                          <input type="submit" value="submit">
                          </form>
                          <script>
                          window.onload = function(){
                          document.getElementById("img").onclick = function(){
                          this.src = "/practice_war/check?"+new Date().getTime();
                          }
                          }

                          </script>
                          </body>
                          </html>
                          + +
                          checkcode
                          @WebServlet("/check")
                          public class ServletDemo extends HttpServlet {
                          @Override
                          protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                          //验证码图片大小
                          final int width = 100;
                          final int height = 50;

                          /* 绘制验证码 */
                          BufferedImage image = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
                          Graphics pen = image.getGraphics();
                          //绘制背景
                          pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
                          pen.fillRect(0,0,width,height);
                          //绘制边框
                          pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
                          pen.drawRect(0,0,width-1,height-1);
                          //随机填充字母数字
                          String source = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1234567890";

                          StringBuilder verifyAnswer = new StringBuilder();

                          for (int i = 1; i <= 4; i++){
                          int index = (int)(Math.random()*source.length());
                          verifyAnswer = verifyAnswer.append(source.charAt(index));
                          pen.drawString(source.substring(index,index+1),20*i,27);
                          }
                          //画干扰色线
                          pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
                          for (int i = 0; i < 5; i++){
                          pen.drawLine((int)(Math.random()*width),(int)(Math.random()*height),(int)(Math.random()*width),(int)(Math.random()*height));
                          }

                          request.getSession().setAttribute("verifycode",verifyAnswer.toString());
                          System.out.println("verify:"+verifyAnswer.toString());
                          //将图片输出
                          ImageIO.write(image,"jpg",response.getOutputStream());
                          }

                          @Override
                          protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                          this.doGet(request, response);
                          }
                          }
                          + +
                          login
                          @WebServlet("/loginServlet")
                          public class loginServlet extends HttpServlet {
                          @Override
                          protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                          HttpSession session = request.getSession();
                          String verifycode = request.getParameter("verifycode");
                          System.out.println(flag);

                          String ans = session.getAttribute("verifycode");
                          if (ans == null||!ans.equals(verifycode)){
                          session.removeAttribute("verifycode");
                          // 重定向到错误界面
                          request.getRequestDispatcher("/fail_code").forward(request,response);
                          return;
                          }
                          session.removeAttribute("verifycode");

                          // 进行密码账号匹配处理
                          String username = request.getParameter("username");
                          String password = request.getParameter("password");
                          System.out.println(username+" "+password);
                          if(UserDao.login(new User(username,password))){
                          // 成功界面
                          request.setAttribute("uname",username);
                          request.getRequestDispatcher("/success").forward(request,response);
                          }else{
                          // 失败界面
                          request.getRequestDispatcher("/fail").forward(request,response);
                          }
                          }

                          @Override
                          protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                          this.doGet(request, response);
                          }
                          }
                          + +

                          老师的写法是将错误信息直接写在原登录界面,和我的略有不同:

                          +
                          // in loginServlet
                          if (!session.getAttribute("verifycode").equals(verifycode)){
                          request.setAttribute("message","checkcode_fail");
                          request.getRequestDispatcher("/login.jsp").forward(request,response);
                          return;
                          }
                          + +
                          // in login.jsp
                          <%
                          String message = (String) request.getAttribute("message");
                          if(message != null){
                          if(message.equals("checkcode_fail")){
                          out.write("验证码错误!");
                          }else if(message.equals("pass_fail")){
                          out.write("用户名或密码错误!");
                          }
                          }
                          %>
                          + +

                          以及success.jsp

                          +

                          image-20230302233110661

                          +
                          成功/两个失败

                          仅以成功为例

                          +
                          @WebServlet(value = "/success")
                          public class SuccessServlet extends HttpServlet {
                          @Override
                          protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                          doPost(req,resp);
                          }

                          @Override
                          protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                          resp.setContentType("text/html;charset=utf-8");
                          resp.getWriter().write("登录成功!"+req.getAttribute("uname")+",欢迎您");
                          }
                          }
                          + +

                          JSP

                          +

                          现在都用 Thymeleaf ,更符合 MVC 的执行过程,也没有 JSP 这种耦合杂乱的页面代码,但是模板引擎的思路大致相同,还是可以看一看的

                          +
                          +

                          改动之后无需重启服务器,刷新界面即可。

                          +
                          +

                          关于热更新的机制可以看看这篇文章,水平有限还看不懂就先放在这了:

                          +

                          JSP热部署的实现原理[通俗易懂]

                          +
                          +

                          概念

                          JSP(Java Server Pages) Java服务器端页面,用于简化书写

                          +

                          可以理解为:一个特殊的页面,其中既可以定义html标签,又可以定义java代码

                          +

                          比如说,上一个案例的Servlet代码就可以直接写入到JSP中,而且response和request这些对象可以直接用

                          +
                          <%@ page import="java.text.SimpleDateFormat" %>
                          <%@ page import="java.util.Date" %>
                          <html>
                          <body>
                          <h2>Hello World!</h2>
                          <%
                          //获取流对象之前,设置流的默认编码:ISO-8859-1 设置为:GBK
                          response.setCharacterEncoding("utf-8");

                          //告诉浏览器,服务器发送的消息体数据的编码。建议浏览器使用该编码解码
                          response.setHeader("content-type","text/html;charset=utf-8");
                          if(request.getCookies() != null)
                          for(Cookie c : request.getCookies()){
                          if(c.getName().equals("isfirst")){
                          //response.getWriter().write("<h1>欢迎回来,您上次访问的时间为<h1>"+c.getValue());
                          response.getWriter().write("<h1>Welcome!The last time you visit is <h1>"+c.getValue());
                          //System.out.println("欢迎回来,您上次访问的时间为"+c.getValue());
                          break;
                          }
                          }
                          else
                          response.getWriter().write("<h1>Hello!Welcome!<h1>");

                          SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");
                          Date date1 = new Date();
                          String currentTime = dateFormat.format(date1);

                          //response.getWriter().write("<h1>你好!欢迎你!<h1>");
                          //System.out.println("你好!欢迎你!");
                          response.addCookie(new Cookie("isfirst",currentTime));
                          %>
                          </body>
                          </html>
                          + +

                          最终效果:

                          +image-20230222230929893 + +

                          原理

                          JSP本质上是Servlet

                          +

                          image-20230222233533332

                          +

                          JSP的脚本

                          JSP定义Java代码的方式

                          +
                            +
                          1. <% 代码 %>

                            +

                            定义的java代码,在service方法中。service方法中可以定义什么,该脚本中就可以定义什么。

                            +

                            也即最后会构成Servlet体

                            +
                          2. +
                          3. <%! 代码 %>

                            +

                            定义的java代码,在jsp转换后的java类的成员位置。可以是成员变量,或者是成员方法。

                            +

                            注:最好不要在Servlet中定义成员变量,否则容易引发线程安全问题。

                            +
                          4. +
                          5. <%= 代码 %>

                            +

                            定义的java代码,会输出到页面上。输出语句中可以定义什么,该脚本中就可以定义什么。

                            +

                            image-20230223000057595

                            +

                            比如说可以用来输出某个变量的值。注意这东西由于本质上是写在Servlet的service方法中的,因而当成员变量和service方法的局部变量重名,会依据就近原则优先使用局部变量的值。

                            +
                          6. +
                          +

                          指令

                          也就是jsp开头那些东西,比如说这个:

                          +
                          <%@ page contentType="text/html;charset=UTF-8" language="java" %>
                          + +

                          用来配置jsp的资源页面信息

                          +
                            +
                          • 分类:
                              +
                            1. page : 配置JSP页面的
                                +
                              • contentType:等同于response.setContentType()
                                contentType="text/html;charset=UTF-8"
                                +
                                  +
                                1. 设置响应体的mime类型以及字符集
                                2. +
                                3. 设置当前jsp页面的编码(只能是高级的IDE才能生效,如果使用低级工具,则需要设置pageEncoding属性设置当前页面的字符集)
                                4. +
                                +
                              • +
                              • import:导包
                              • +
                              • errorPage:当前页面发生异常后,会自动跳转到指定的错误页面
                              • +
                              • isErrorPage:标识当前页面是否是错误页面。
                                  +
                                • true:是,可以使用内置对象exception【用来获取异常名称/信息等】
                                • +
                                • false:否。默认值。不可以使用内置对象exception
                                • +
                                +
                              • +
                              +
                            2. +
                            3. include : 页面包含的。导入页面的资源文件,可以引入其它的jsp或者html,引入之后就会展示同样的内容。
                                +
                              • <%@include file=”top.jsp”%>
                              • +
                              +
                            4. +
                            5. taglib导入资源。用来导入标签库
                                  * <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
                              +        * prefix:前缀,自定义的。之后就可以用`<c:XXX>`了。相当于什么std::。
                              +
                              +
                            6. +
                            +
                          • +
                          +

                          中文乱码

                          但是注意一点

                          +
                          //获取流对象之前,设置流的默认编码:ISO-8859-1 设置为:GBK
                          response.setCharacterEncoding("utf-8");

                          //告诉浏览器,服务器发送的消息体数据的编码。建议浏览器使用该编码解码
                          response.setHeader("content-type","text/html;charset=utf-8");
                          + +

                          这样做在Servlet不会导致中文乱码,但JSP不行,这个大概是因为两者原理不一样。

                          +

                          Servlet的中文乱码:

                          +image-20230222231756277 + +

                          JSP的:

                          +

                          image-20230222231820358

                          +

                          Servlet乱码是因为客户端和response请求体编码不一致,JSP乱码与JSP的原理有关,是只跟服务器端有关

                          +
                          +

                          编译jsp有以下几个步骤:
                          (1)把jsp转化为java源码。pageEncoding=xxx指定以xxx编码格式读取jsp文件,因此,jsp文件的编码格式应与pageEncoding值一致。
                          (2)把java源码编译为字节码,即.class文件。转化后的java源码为utf-8编码格式,字节码也为utf-8编码,我们无需干预。
                          (3)执行.class文件。在此过程,需向浏览器发送中文字符,contentType=xxx指定了jsp以xxx编码显示字符。也就是在浏览器中查看页面编码,其值为contentType指定的编码。

                          +

                          因此,在1、3环节,**只要指定一致的编码格式(jsp文件编码格式=pageEncoding=contentType)**,即可保证jsp页面不出现乱码。
                          举例:jsp文件以utf-8格式编写,那么pageEncoding=utf-8, contentType=utf-8,就保证了jsp页面不出现乱码。
                          ————————————————
                          版权声明:本文为CSDN博主「liuhaidl」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
                          原文链接:https://blog.csdn.net/liuhaidl/article/details/84012372

                          +
                          +

                          指定方法是在JSP开头添加:

                          +
                          <%@ page pageEncoding="UTF-8"%>
                          + +

                          内置对象

                          在jsp页面中不需要获取和创建,可以直接使用的对象。

                          +

                          jsp一共有9个内置对象。

                          +
                            +
                          1. request HttpServletRequest 一次请求访问的多个资源(转发)

                            +
                          2. +
                          3. response HttpServletResponse 响应对象

                            +
                          4. +
                          5. out: JspWriter 字符输出流对象。可以将数据输出到页面上。和response.getWriter()类似

                            +
                              +
                            • response.getWriter()和out.write()的区别: 在tomcat服务器真正给客户端做出响应之前,会先找response缓冲区数据,再找out缓冲区数据。因而response.getWriter()数据输出永远在out.write()之前 所以说,用out更好,因为它跟随你布局变化,你out写在哪,这句话最终就会输出在哪
                            • +
                            +
                          6. +
                          7. pageContext PageContext 当前页面共享数据,还可以获取其他八个内置对象

                            +
                          8. +
                          9. session HttpSession 一次会话的多个请求间

                            +
                          10. +
                          11. application ServletContext 所有用户间共享数据

                            +
                          12. +
                          13. page Object 当前页面(Servlet)的对象,相当于this

                            +
                          14. +
                          15. config ServletConfig Servlet的配置对象

                            +
                          16. +
                          17. exception Throwable 异常对象。只在page指令的isErrorPage为true的情况下才能使用此对象。

                            +
                          18. +
                          +

                          其中,

                          +

                          image-20230307144539506

                          +

                          这四个为用来共享数据的域对象

                          +

                          演变:MVC开发模式

                          jsp的演变

                          image-20230307145442383

                          +

                          MVC模式

                          将程序分成三个部分,分别是M-V-C。

                          +
                            +
                          1. M:Model,模型。JavaBean
                              +
                            • 完成具体的业务操作,如:查询数据库,封装对象
                            • +
                            +
                          2. +
                          3. V:View,视图。JSP
                              +
                            • 展示数据
                            • +
                            +
                          4. +
                          5. C:Controller,控制器。Servlet
                              +
                            • 获取用户的输入
                            • +
                            • 调用模型
                            • +
                            • 将数据交给视图进行展示【域对象共享数据】
                            • +
                            +
                          6. +
                          +

                          image-20230307150845273

                          +

                          服务器将接收的请求给控制器处理,控制器控制model完成必要的运算,model把算出的东西返回给控制器,控制器再把数据交给视图展示,数据最终就回到了浏览器客户端。

                          +

                          这就算是一个微型CPU了吧,控制器就是CU,模型就是ALU,也许客户端和视图什么的可以视为IO接口。

                          +
                            +
                          • 优缺点:

                            +
                              +
                            1. 优点:

                              +
                                +
                              1. 耦合性低,方便维护,可以利于分工协作
                              2. +
                              3. 重用性高
                              4. +
                              +
                            2. +
                            3. 缺点:

                              +
                                +
                              1. 使得项目架构变得复杂,对开发人员要求高
                              2. +
                              +
                            4. +
                            +
                          • +
                          +

                          那么,我们可以知道,jsp就只需负责数据的展示了。那怎么展示数据呢?这就需要用到jsp的几个技术了:

                          +

                          EL表达式

                          +

                          注意,servlet3.0以来默认关闭el表达式解析,需要手动在page上加属性打开,详见 jsp文件中的el表达式失效问题解决

                          +
                          +

                          Expression language,替换和简化jsp上java代码的书写

                          +

                          语法:${表达式}

                          +

                          jsp会执行里面的表达式,然后把结果输出。

                          +

                          image-20230307151706211

                          +

                          加反斜杠可忽略。

                          +

                          使用场景:

                          +
                            +
                          1. 运算

                            +
                                  1. 算数运算符: + - *  / %
                            +      2. 比较运算符: > < >= <= == !=
                            +      3. 逻辑运算符: && || !
                            +      4. 空运算符: empty
                            +   * 功能:用于判断字符串、集合、数组对象是否为null**或者**长度是否为0
                            +   * `${empty 变量名}`: 判断字符串、集合、数组对象是否为null或者长度为0
                            +   * `${not empty 变量名}`: 表示判断字符串、集合、数组对象是否不为null 并且 长度>0
                            +
                            +
                          2. +
                          3. 获取值

                            +
                              +
                            1. el表达式只能从域对象中获取值

                              +

                              image-20230307144539506

                              +
                            2. +
                            3. 语法:

                              +
                                +
                              1. ${域名称.键名}:从指定域中获取指定键的值
                              2. +
                              +
                                +
                              • 域名称:
                                  +
                                1. pageScope –> pageContext
                                2. +
                                3. requestScope –> request
                                4. +
                                5. sessionScope –> session
                                6. +
                                7. applicationScope –> application(ServletContext)
                                8. +
                                +
                              • +
                              • 举例:在request域中存储了name=张三,获取:${requestScope.name}
                              • +
                              +
                                +
                              1. ${键名}:表示依次从最小的域中查找是否有该键对应的值,直到找到为止。
                              2. +
                              +
                            4. +
                            5. 案例

                              +

                              这样一来,访问/demo就能转发到index.jsp,显示出属性值

                              +
                                +
                              1. Servlet

                                +
                                protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
                                request.setAttribute("name","xiunian");
                                request.getRequestDispatcher("/index.jsp").forward(request,response);
                                }
                              2. +
                              3. index.jsp

                                +
                                <%@ page pageEncoding="UTF-8" isELIgnored="false" %>
                                <html>
                                <body>
                                <h2>Hello World!</h2>
                                ${requestScope.name}
                                </body>
                                </html>
                              4. +
                              +
                            6. +
                            7. 获取非字符串类型的值

                              +
                                +
                              1. 对象

                                +
                              2. +
                              3. 集合(List、Map等)

                                +
                              4. +
                              +
                            8. +
                            9. +
                            +
                          4. +
                          +]]>
                          + + Java + +
                          + + 密码学基础 + /2023/11/26/cryptography/ + +

                          学习目的:顺利过考试,以及获取基本的密码学知识,数学原理不重要

                          + +

                          第一章 概述

                          image

                          +

                          image

                          +

                          image

                          +

                          image

                          +

                          image

                          +

                          image

                          +

                          image

                          +

                          第二章 传统密码技术

                          概念

                          image

                          +

                          image

                          +

                          分类

                          置换密码

                          image

                          +

                          列置换密码

                          加密

                          image

                          +
                          解密

                          image

                          +
                          例子

                          image

                          +

                          image

                          +

                          周期置换密码

                          image

                          +

                          image

                          +

                          代换密码

                          image

                          +

                          单表代换

                          image

                          +

                          image

                          +

                          多表代换

                          image

                          +

                          image

                          +

                          image

                          +

                          image

                          +

                          传统密码体制分析

                          频率(单表代换)

                          image

                          +

                          重合指数(多表代换)

                          image

                          +

                          明文-密文对(hill密码)

                          image

                          +

                          第三章 分组密码-DES

                          概述

                          image

                          +
                            +
                          1. 分组密码一般指对称分组密码
                          2. +
                          +

                          image

                          +
                            +
                          1. 明文经编码表示后变成二进制序列
                          2. +
                          3. 二进制序列固定长度分组
                          4. +
                          5. 每组在密钥控制下转为密文分组
                          6. +
                          7. 本质上是明文到密文的一一映射
                          8. +
                          9. 一般明文长度=密文长度,密钥长度不一定
                          10. +
                          +

                          image

                          +

                          image

                          +

                          设计思想

                          image

                          +

                          image

                          +

                          image

                          +

                          image

                          +

                          image

                          +

                          基本特点

                          image

                          +

                          子密钥生成算法

                          image

                          +

                          轮函数

                          image

                          +

                          迭代轮数

                          image

                          +

                          DES算法

                          概述

                          image

                          +

                          image

                          +

                          加密流程

                          总体流程

                          image

                          +

                          image

                          +

                          image

                          +

                          选择扩展置换E

                          image

                          +

                          子密钥生成

                          image

                          +

                          压缩替代S-盒

                          image

                          +

                          image

                          +

                          image

                          +

                          置换p-盒

                          image

                          +

                          解密流程

                          image

                          +

                          image

                          +

                          安全性分析

                          image

                          +

                          互补性

                          image

                          +

                          image

                          +

                          弱密钥

                          image

                          +

                          image

                          +

                          差分分析

                          只有理论上意义

                          +

                          image

                          +

                          线性分析

                          实际上不可行

                          +

                          image

                          +

                          密钥搜索

                          image

                          +

                          image

                          +

                          多重DES

                          image

                          +

                          image

                          +

                          二重

                          image

                          +

                          3DES

                          你也是过渡阶段?

                          +

                          image

                          +

                          第四章 有限域

                          数学基础

                          image

                          +

                          逆元:

                          +

                          image-20231119235319197

                          +

                          比如说在G(7)中,2的逆元为4。

                          +

                          也即,任意整数a,则存在x,a / 2 == a * 4 (mod 7),4为2模7的乘法逆元,记为 2(-1)(mod 7) = 4。

                          +

                          image

                          +

                          求逆元的方法是求b^(m-2) mod m。如2^(5) mod 7 = 4。

                          +

                          群环域

                          image

                          +

                          image

                          +

                          image

                          +

                          确实封闭且结合且单位元且逆元

                          +

                          循环群

                          image

                          +

                          image

                          +

                          image

                          +

                          确实是环

                          +

                          image

                          +

                          image

                          +

                          有限域GF(p)

                          有限域就是阶为素数幂的域?

                          +

                          image

                          +

                          image

                          +

                          image

                          +

                          image-20231119233220659

                          +

                          多项式运算

                          image

                          +

                          普通多项式运算

                          image

                          +

                          image

                          +

                          image

                          +

                          image

                          +

                          系数模p运算的多项式运算

                          image

                          +

                          确实,毕竟系数本身就是域了,除了没定义逆元外别的都满足。

                          +

                          image

                          +

                          image

                          +

                          有限域GF(2^n)

                          image

                          +

                          image

                          +

                          image

                          +

                          第五章 高级加密标准-AES

                          概述

                          简介

                          image

                          +

                          image

                          +

                          Nr=Nk的幂数x2

                          +

                          简化版AES

                          image

                          +

                          image

                          +

                          具体算法详见PPT。

                          +

                          基本结构

                          image

                          +

                          总体流程

                          image

                          +

                          加密流程

                          整体流程

                          image

                          +

                          image

                          +

                          image

                          +

                          状态矩阵

                          image

                          +

                          字节代替

                          image

                          +

                          行移位

                          image

                          +

                          列混淆

                          image

                          +

                          image

                          +

                          可以关注下是怎么通过C矩阵求出这个固定多项式的:

                          +

                          image

                          +

                          轮密钥加

                          image

                          +

                          密钥扩展

                          image

                          +

                          image

                          +

                          感觉也是类似对明文做的操作

                          +

                          安全评估

                          image

                          +

                          image

                          +

                          image

                          +

                          image

                          SM4

                          image

                          image

                          第六章 分组密码的工作模式

                          image

                          @@ -6947,6 +5904,824 @@ url访问填写http://localhost/webdemo4_war/*.do
                        4. image

                          image

                          image

                          +]]> + + + 计算机组成原理 + /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

                          +

                          一般栈顶地址最低。

                          +
                          +

                          运算方法

                          定点运算

                          一位乘法运算

                          原码一位乘

                          +

                          image-20230621195833297

                          +

                          大致明白了:

                          +

                          ①乘积一共有四位,故而需要两个寄存器来保存。

                          +

                          ②按照上面的原理公式,每次右移一位,被移出的那一位也是最后的结果(相当于竖式中每次相加的最后一位),需要把它存储在另一个寄存器中。

                          +

                          ③我们选择了存乘数的寄存器,因为乘数已经乘过的位是没用的。存乘数的那个寄存器的乘数不断被结果的低位所替代。

                          +

                          故****基本流程****:

                          +

                          ①准备阶段:清零ACC【置部分积=0】,在MQ中放乘数,X中放被乘数

                          +

                          ②判断MQ中乘数最低位,若为1,则ACC部分积加上X中的被乘数;若为0,则ACC不变

                          +

                          ③将ACC和MQ中四位数字视作一个整体,符号位也算上,进行逻辑右移,左侧补0.

                          +

                          ④重复上述过程,按移位次数来控制结束。

                          +

                          ⑤则最后,ACC中存储的就是乘法结果的高位,MQ中存储的结果就是乘法中的低位。

                          +

                          这其实就是我们用的列竖式一行一行加起来的一个过程。

                          +

                          img

                          +

                          S是符号位,GM是乘法标志位。

                          +

                          控制门:当最后一位是1时,控制门打开,X中的被乘数进入加法器。

                          +
                          +
                            +
                          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表示是否溢出。

                          +
                          +
                            +
                          1. 被除数(余数) 商

                            +
                          2. +
                          3. 先加上 - 除数的补

                            +
                          4. +
                          5. 如果得到结果≥0,则上商1,左移,下一次继续加 - 除数的补

                            +
                          6. +
                          7. 如果小于0,则上商0,左移,下一次加除数的补

                            +

                            image-20230620234054088

                            +

                            逻辑左移

                            +
                          8. +
                          9. 左移5次(商包括符号位的所有数字被填满),最后一次上商不用移位

                            +
                          10. +
                          +

                          浮点运算

                          舍入

                          +

                          (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. 基本概念
                          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。

                          +
                          +

                          流水线处理器

                          流水线概述

                          流水线

                          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. 分支预测

                            +
                              +
                            1. 静态分支预测

                              +

                              image-20230618153106322

                              +
                            2. +
                            3. 动态分支预测

                              +

                              image-20230618153125295

                              +
                            4. +
                            +
                          4. +
                          +

                          流水线数据通路和控制

                          流水线数据通路

                          流水线寄存器

                          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寄存器内容改变

                            +

                            下一条指令就能重新取指

                            +
                          4. +
                          +

                          控制冒险

                          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. +
                          3. 间址周期

                            +

                            image-20230621201351939

                            +
                          4. +
                          5. 执行周期 ①访存指令 ②非访存指令 ③转移指令 ④三类指令的指令周期

                            +

                            image-20230621201358396

                            +

                            imgimg

                            +

                            image-20230621201423245

                            +
                          6. +
                          7. 中断周期 硬件法和软件法

                            +

                            imgimg

                            +

                            硬件和软件法。

                            +
                          8. +
                          +

                          二、控制单元的功能

                          +
                            +
                          1. 输入信号

                            +

                            ①时钟信号 ②指令寄存器【控制信号与操作码有关】 ③标志 ④外来信号【中断请求、总线请求】

                            +
                          2. +
                          3. 输出信号

                            +

                            ①CPU内各种控制信号【比如(PC)+1->PC这种】

                            +

                            ②送至控制总线的信号【比如中断响应、总线响应】

                            +
                          4. +
                          5. 控制信号举例

                            +

                            ①不使用内部总线

                            +

                            ②采用内部总线

                            +
                          6. +
                          7. 多级时序系统

                            +
                              +
                            1. 机器周期

                              +

                              取指周期=机器周期=最复杂的微操作所需时间【访存】

                              +

                              在机器周期内部也需要有时钟来控制微操作的执行顺序

                              +
                            2. +
                            3. 时钟周期(节拍、状态)

                              +

                              每个指令周期都可分为若干个机器周期,每个机器周期都可分为若干个节拍(时钟周期)。一个机器周期内包含多少节拍与需要发送多少控制信号、控制信号复杂度、控制信号能否并行有关。

                              +

                              时钟产生节拍信号,不同的节拍信号有不同的先后顺序。

                              +

                              一个时钟周期产生一个或几个【并行的几个,或者是操作时间很短,虽然有一定的先后顺序,但可以在一个节拍内完成】微操作命令

                              +

                              时钟信号利用上升沿让CU发出控制命令【微操作】控制各个不同部件。

                              +
                            4. +
                            +
                          8. +
                          9. 控制方式

                            +

                            ①同步控制方式 采用定长的机器周期、不定长的机器周期、中央控制和局部控制相结合

                            +

                            ​ 当指令大多都是可以提前确定的,就用同步。当一条微操作的时间很难控制,可以采用异步控制。

                            +

                            ②异步控制方式 等待IO读写

                            +

                            ③联合控制方式 同步与异步结合

                            +

                            ④人工控制

                            +
                          10. +
                          +

                          三、组合逻辑设计

                          +
                            +
                          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. +
                          5. EPROM(多次性编程)

                            +

                            image-20230618223151914

                            +
                          6. +
                          7. EEPROM(电可擦写)

                            +

                            image-20230618223229585

                            +
                          8. +
                          9. Flash Memory(闪速型存储器)

                            +

                            image-20230618223252821

                            +
                          10. +
                          +

                          存储器与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. INTR默认为0,取非为1. 经&后整个排队电路为1
                          2. +
                          3. 当i设备发出请求,INTRi=1,取非为0,经&后变为0,INTPi之后的电路清零,只有i之前的INTP为1
                          4. +
                          5. 3在一连串的显示为 1 的INTP中,最后一个显示1的设备优先级最高。因为按照我们的分析,是它发出了请求
                          6. +
                          +

                          使用与非+非而不是直接与门是因为与非门+非更便宜。

                          +

                          我猜这个意思是,链式排队的话,越前面的优先级越高,现在我们讲的是怎么快速****找出****最高的最前面的是哪一个。之所以为什么越前面的优先级最高,可从这个电路中得知。如果一个东西发出请求,那么它后面的INTPi’都会被置零,因而它肯定比它后面的高级。因此越前面的优先级越高。

                          +

                          https://www.likecs.com/show-390301.html

                          +

                          img

                          +

                          这个可以验证我的观点。至于这个轮询方式,应该在第三章的总线那边讲过,应该用的是链式查询。

                          +
                          +
                          软件实现

                          程序查询

                          +

                          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

                          ]]>
                          @@ -6995,85 +6770,244 @@ url访问填写http://localhost/webdemo4_war/*.do

                          具体实现

                          构造器的参数

                          参考文章

                          也是系统调用socket的参数,了解一下知识多多益善。

                            -
                          1. domain

                            -

                            在本次实验中只会取值前两个,即本地通信和IPv4网络通信

                            -

                            image-20230309232045195

                            +
                          2. domain

                            +

                            在本次实验中只会取值前两个,即本地通信和IPv4网络通信

                            +

                            image-20230309232045195

                            +
                          3. +
                          4. type

                            +

                            好像比如说取SOCK_DGRAM就是UDP,取SOCK_STREAM就是TCP。

                            +
                          5. +
                          +
                          代码
                          /* Socket */
                          /* 构造器 */
                          // default constructor for socket of (subclassed) domain and type
                          Socket::Socket(const int domain, const int type) : FileDescriptor(SystemCall("socket", socket(domain, type, 0))) {}
                          // construct from file descriptor
                          Socket::Socket(FileDescriptor &&fd, const int domain, const int type) : FileDescriptor(move(fd)) { ... }

                          // get the local or peer address the socket is connected to
                          // 此为private函数,应该是用于方便下面那两个函数的,虽然我觉得这个设计意图没什么必要()
                          Address Socket::get_address(const string &name_of_function,const function<int(int, sockaddr *, socklen_t *)> &function) const {
                          Address::Raw address;
                          socklen_t size = sizeof(address);
                          SystemCall(name_of_function, function(fd_num(), address, &size));
                          return {address, size};
                          }
                          Address Socket::local_address() const { return get_address("getsockname", getsockname); }
                          Address Socket::peer_address() const { return get_address("getpeername", getpeername); }

                          /*
                          这两个函数是用于把socket连到CS的
                          将socket的一端连上本机,就需要调用bind;连上别的什么东西就要用connect
                          */
                          // bind socket to a specified local address (usually to listen/accept)
                          // address is a local Address to bind
                          void Socket::bind(const Address &address) { SystemCall("bind", ::bind(fd_num(), address, address.size())); }
                          // connect socket to a specified peer address
                          // address is the peer's Address
                          void Socket::connect(const Address &address) { SystemCall("connect", ::connect(fd_num(), address, address.size())); }

                          // shut down a socket in the specified way
                          // how can be `SHUT_RD`, `SHUT_WR`, or `SHUT_RDWR`
                          void Socket::shutdown(const int how) {
                          SystemCall("shutdown", ::shutdown(fd_num(), how));
                          switch (how) {
                          case SHUT_RD:
                          register_read();
                          break;
                          // ...
                          }
                          }

                          // set socket option,传入协议层以及要设置非选项的键和值
                          template <typename option_type>
                          void Socket::setsockopt(const int level, const int option, const option_type &option_value) {
                          SystemCall("setsockopt", ::setsockopt(fd_num(), level, option, &option_value, sizeof(option_value)));
                          }

                          // allow local address to be reused sooner, at the cost of some robustness
                          // 以鲁棒性为代价,让local address可复用
                          // Using `SO_REUSEADDR` may reduce the robustness of your application
                          void Socket::set_reuseaddr() { setsockopt(SOL_SOCKET, SO_REUSEADDR, int(true)); }

                          /* UDPSocket */
                          // 从socket中接收数据并放进datagram中
                          // If mtu is too small to hold the received datagram, this method throws a runtime_error
                          void UDPSocket::recv(received_datagram &datagram, const size_t mtu) {
                          // receive source address and payload
                          // ...
                          const ssize_t recv_len = SystemCall(
                          "recvfrom",
                          ::recvfrom(
                          fd_num(), datagram.payload.data(), datagram.payload.size(), MSG_TRUNC, datagram_source_address, &fromlen));
                          // ...
                          }
                          UDPSocket::received_datagram UDPSocket::recv(const size_t mtu) {
                          received_datagram ret{{nullptr, 0}, ""};
                          recv(ret, mtu);
                          return ret;
                          }

                          // 向socket发送数据
                          void sendmsg_helper(const int fd_num,
                          const sockaddr *destination_address,
                          const socklen_t destination_address_len,
                          const BufferViewList &payload) {
                          // ...
                          const ssize_t bytes_sent = SystemCall("sendmsg", ::sendmsg(fd_num, &message, 0));
                          // ...
                          }
                          void UDPSocket::sendto(const Address &destination, const BufferViewList &payload) {
                          sendmsg_helper(fd_num(), destination, destination.size(), payload);
                          register_write();
                          }
                          void UDPSocket::send(const BufferViewList &payload) {
                          sendmsg_helper(fd_num(), nullptr, 0, payload);
                          register_write();
                          }
                          // ...
                          + +

                          * TCPSpongeSocket

                          上面那俩类其实就是两个包装类,用来将系统调用包装为c++类,看起来很抽象很迷惑。但到这就不一样了!我们开始用上我们之前写的TCP协议的代码了!

                          +

                          除了跟fd以及socket一致的readwrite以及close之外,TCPSocket最独特的功能,应该就是TCP连接的建立与释放了,其状态转移等逻辑已由我们在Lab0-4实现,此socket类仅实现事件的监听TCP协议对象生命周期的管理

                          +

                          双线程

                          在详细说明其两个功能——事件监听和生命周期管理——之前,不妨先了解下其总体的架构。

                          +

                          TCPSpongeSocket需要双线程实现。其中一个线程用来招待其owner:它会执行向owner public的connect、read、write等服务。另一个线程用来运行TCPConnection:它会时刻调用connection的tick方法,并且进行事件监听。

                          +
                          //! \class TCPSpongeSocket
                          //! This class involves the simultaneous operation of two threads.
                          //!
                          //! One, the "owner" or foreground thread, interacts with this class in much the
                          //! same way as one would interact with a TCPSocket: it connects or listens, writes to
                          //! and reads from a reliable data stream, etc. Only the owner thread calls public
                          //! methods of this class.
                          //!
                          //! The other, the "TCPConnection" thread, takes care of the back-end tasks that the kernel would
                          //! perform for a TCPSocket: reading and parsing datagrams from the wire, filtering out
                          //! segments unrelated to the connection, etc.
                          + +

                          事件监听

                          完成事件监听的核心部分是方法_tcp_loop以及_initialize_TCP中对_eventloop的初始化,还有eventloop的实现。

                          +

                          看下来其实理解难度不大(虽然细节很多并且我懒得研究了),但我认为很值得学习。

                          +
                          _initialize_TCP

                          主要功能是添加我们想监听的事件,有四个,分别是从app得到数据、有要向app发送的数据、从底层协议得到数据、有要向底层协议发送的数据。具体的话,代码和注释都写得很详细就不说了。

                          +

                          可以看到,TCP与协议栈交互【包括收发数据报】,是通过AdaptT _datagram_adapter;实现的;TCP与上层APP交互【包括传送数据】,是通过LocalStreamSocket _thread_data;实现的。

                          +
                          template <typename AdaptT>
                          void TCPSpongeSocket<AdaptT>::_initialize_TCP(const TCPConfig &config) {
                          _tcp.emplace(config);
                          // Set up the event loop

                          // There are four possible events to handle:需要监听以下四种事件
                          //
                          // 1) Incoming datagram received (needs to be given to
                          // TCPConnection::segment_received method)得到底层协议栈送过来的data
                          //
                          // 2) Outbound bytes received from local application via a write()
                          // call (needs to be read from the local stream socket and
                          // given to TCPConnection::data_written method)得到上层app送过来的data
                          //
                          // 3) Incoming bytes reassembled by the TCPConnection
                          // (needs to be read from the inbound_stream and written
                          // to the local stream socket back to the application)TCP协议需要向app写入data
                          //
                          // 4) Outbound segment generated by TCP (needs to be
                          // given to underlying datagram socket)TCP需要向外界发送data

                          // rule 1: read from filtered packet stream and dump into TCPConnection得到外界data
                          _eventloop.add_rule(_datagram_adapter,
                          Direction::In,
                          [&] {
                          auto seg = _datagram_adapter.read();
                          if (seg) {
                          _tcp->segment_received(move(seg.value()));
                          }
                          if (_thread_data.eof() and _tcp.value().bytes_in_flight() == 0 and not _fully_acked) { _fully_acked = true; }
                          },
                          [&] { return _tcp->active(); });

                          // rule 2: read from pipe into outbound buffer得到app data
                          _eventloop.add_rule(
                          // LocalStreamSocket _thread_data;
                          // 看来用户是通过socket写入的数据
                          _thread_data,
                          Direction::In,
                          [&] {
                          const auto data = _thread_data.read(_tcp->remaining_outbound_capacity());
                          const auto len = data.size();
                          const auto amount_written = _tcp->write(move(data));
                          if (amount_written != len) {
                          throw runtime_error("TCPConnection::write() accepted less than advertised length");
                          }
                          if (_thread_data.eof()) {
                          _tcp->end_input_stream();
                          _outbound_shutdown = true;
                          }
                          },
                          [&] { return (_tcp->active()) and (not _outbound_shutdown) and (_tcp->remaining_outbound_capacity() > 0); },
                          [&] {
                          _tcp->end_input_stream();
                          _outbound_shutdown = true;
                          });

                          // rule 3: read from inbound buffer into pipe向app写入data
                          _eventloop.add_rule(
                          _thread_data,
                          Direction::Out,
                          [&] {
                          ByteStream &inbound = _tcp->inbound_stream();
                          // Write from the inbound_stream into the pipe
                          const size_t amount_to_write = min(size_t(65536), inbound.buffer_size());
                          const std::string buffer = inbound.peek_output(amount_to_write);
                          // 通过向socket写实现
                          const auto bytes_written = _thread_data.write(move(buffer), false);
                          inbound.pop_output(bytes_written);

                          if (inbound.eof() or inbound.error()) {
                          _thread_data.shutdown(SHUT_WR);
                          _inbound_shutdown = true;
                          }
                          },
                          [&] {
                          return (not _tcp->inbound_stream().buffer_empty()) or
                          ((_tcp->inbound_stream().eof() or _tcp->inbound_stream().error()) and not _inbound_shutdown);
                          });

                          // rule 4: read outbound segments from TCPConnection and send as datagrams向外界写data
                          _eventloop.add_rule(_datagram_adapter,
                          Direction::Out,
                          [&] {
                          while (not _tcp->segments_out().empty()) {
                          // 通过对adapter写实现
                          _datagram_adapter.write(_tcp->segments_out().front());
                          _tcp->segments_out().pop();
                          }
                          },
                          [&] { return not _tcp->segments_out().empty(); });
                          }
                          + +
                          _tcp_loop

                          可以看到,_tcp_loop的功能就是,在condition为真的时候,一是监听我们之前塞进_event_loop的所有事件,二是调用TCPConnectiontick方法来管理时间。

                          +
                          // condition is a function returning true if loop should continue
                          // Process events while specified condition is true
                          // 周期性调用事件condition以达到监听等待事件的效果,管理TCP的tick
                          template <typename AdaptT>
                          void TCPSpongeSocket<AdaptT>::_tcp_loop(const function<bool()> &condition) {
                          auto base_time = timestamp_ms();
                          // 当条件一直为真时,监听event
                          while (condition()) {
                          // 持续监听eventloop中的各种event
                          auto ret = _eventloop.wait_next_event(TCP_TICK_MS);
                          // 条件为退出/丢弃
                          if (ret == EventLoop::Result::Exit or _abort) {
                          break;
                          }
                          // 如果tcp还存活,则调用其tick方法
                          if (_tcp.value().active()) {
                          const auto next_time = timestamp_ms();
                          _tcp.value().tick(next_time - base_time);
                          _datagram_adapter.tick(next_time - base_time);
                          base_time = next_time;
                          }
                          }
                          }
                          + +
                          eventloop

                          eventloop具体是通过Linux提供的poll机制来进行事件监听的。

                          +
                          +

                          Linux poll机制

                          +

                          怎么说,又一次感受到了“网络就是IO”这个抽象的牛逼之处。操作系统的poll机制和poll函数本质上是针对IO读写来设计的,而正因为网络的本质是IO,正因为网络收发数据包、与上层app交互本质还是IO(因为通过文件描述符),才能在这里采用这种方式进行文件读写。

                          +

                          我的评价是佩服到五体投地好吧

                          +

                          image-20230310185319115

                          +

                          poll函数就是IO等待的一种实现机制。

                          +
                          int poll(struct pollfd *fds, nfds_t nfds, int timeout);
                          + +

                          事件类型events可以为下列值:

                          +
                          POLLIN:有数据可读
                          POLLRDNORM:有普通数据可读,等效于POLLIN
                          POLLRDBAND:有优先数据可读
                          POLLPRI:有紧迫数据可读
                          POLLOUT:写数据不会导致阻塞
                          POLLWRNORM:写普通数据不会导致阻塞
                          POLLWRBAND:写优先数据不会导致阻塞
                          POLLMSG:SIGPOLL消息可用
                          POLLER:指定的文件描述符发生错误
                          POLLHUP:指定的文件描述符挂起事件
                          POLLNVAL:无效的请求,打不开指定的文件描述符
                          +
                          +

                          我们在前面的eventloop的rule初始化中:

                          +
                          _eventloop.add_rule(_datagram_adapter,
                          Direction::In,
                          [&] { ... });
                          + +

                          这个的意思是针对_datagram_adapter这个文件的Direction::In这个事件发生时,就会执行[&]中的事件。那么Direction::In是什么?

                          +
                          enum class Direction : short {
                          In = POLLIN, //!< Callback will be triggered when Rule::fd is readable.
                          Out = POLLOUT //!< Callback will be triggered when Rule::fd is writable.
                          };
                          + +

                          可见,eventloop具体是通过os提供的IO事件机制来进行监听的。

                          +

                          具体的监听以及执行逻辑由wait_next_event来实现。它主要干的就是,清理掉那些我们不感兴趣的或者已经似了(比如说对应的fd已经close之类的)的事件,然后找到那些触发到了的active的事件并且调用它们的caller。

                          +

                          具体代码还是有些微复杂的,有兴趣可以去看看,这里就不放了。

                          +

                          生命周期的管理

                          核心部分为方法connectlisten_and_accept以及_tcp_main

                          +
                          connect

                          由客户端调用。

                          +
                          // Client调用
                          // 未收到外界连接时,owner进程会阻塞
                          template <typename AdaptT>
                          void TCPSpongeSocket<AdaptT>::connect(const TCPConfig &c_tcp, const FdAdapterConfig &c_ad) {
                          // 初始化tcp的事件监听
                          _initialize_TCP(c_tcp);
                          // 初始化adapater
                          _datagram_adapter.config_mut() = c_ad;

                          cerr << "DEBUG: Connecting to " << c_ad.destination.to_string() << "...\n";
                          // 我们实现的:发送SYN报文
                          _tcp->connect();

                          // 统一的状态管理
                          const TCPState expected_state = TCPState::State::SYN_SENT;
                          // 等待直到条件为假,也即脱离SYN-SENT转移到ESTABLISHED
                          _tcp_loop([&] { return _tcp->state() == TCPState::State::SYN_SENT; });
                          cerr << "Successfully connected to " << c_ad.destination.to_string() << ".\n";

                          // 建立连接后开启connection进程, 执行_tcp_main,继续监听event直到死亡
                          _tcp_thread = thread(&TCPSpongeSocket::_tcp_main, this);
                          }
                          + +
                          _tcp_main

                          负责establish状态的监听以及之后关闭TCP连接的擦屁股工作

                          +
                          template <typename AdaptT>
                          void TCPSpongeSocket<AdaptT>::_tcp_main() {
                          try {
                          if (not _tcp.has_value()) {
                          throw runtime_error("no TCP");
                          }
                          // 持续监听直到死亡
                          _tcp_loop([] { return true; });
                          shutdown(SHUT_RDWR);
                          if (not _tcp.value().active()) {
                          cerr << "DEBUG: TCP connection finished "
                          << (_tcp.value().state() == TCPState::State::RESET ? "uncleanly" : "cleanly.\n");
                          }
                          _tcp.reset();
                          } catch (const exception &e) {
                          cerr << "Exception in TCPConnection runner thread: " << e.what() << "\n";
                          throw e;
                          }
                          }
                          + +
                          listen_and_accept

                          由服务器端调用。

                          +
                          // Server调用
                          // 未收到外界连接时,owner进程会阻塞
                          template <typename AdaptT>
                          void TCPSpongeSocket<AdaptT>::listen_and_accept(const TCPConfig &c_tcp, const FdAdapterConfig &c_ad) {
                          _initialize_TCP(c_tcp);
                          _datagram_adapter.config_mut() = c_ad;

                          _datagram_adapter.set_listening(true);

                          cerr << "DEBUG: Listening for incoming connection...\n";
                          // 等待直到ESTABLISHED。注意下这里的状态条件
                          // 其中各种收发报文的事件由tcp_loop中的event做
                          _tcp_loop([&] {
                          const auto s = _tcp->state();
                          return (s == TCPState::State::LISTEN or s == TCPState::State::SYN_RCVD or s == TCPState::State::SYN_SENT);
                          });
                          cerr << "New connection from " << _datagram_adapter.config().destination.to_string() << ".\n";

                          // 开启connection进程
                          _tcp_thread = thread(&TCPSpongeSocket::_tcp_main, this);
                          }
                          + +

                          CS144TCPSocket 和 FullStackSocket

                          主菜(上面那个)已经说完了,这两个就是简单的包装类,没什么好说的,大概就做了点传参工作,主要差异还是adapter。

                          +

                          Adapter实现

                          在我们的TCPSpongeSocket实现中,我们引入了“adapter”的概念。

                          +
                            protected:
                          //! Adapter to underlying datagram socket (e.g., UDP or IP)
                          AdaptT _datagram_adapter;

                          using TCPOverUDPSpongeSocket = TCPSpongeSocket<TCPOverUDPSocketAdapter>;
                          using TCPOverIPv4SpongeSocket = TCPSpongeSocket<TCPOverIPv4OverTunFdAdapter>;
                          using TCPOverIPv4OverEthernetSpongeSocket = TCPSpongeSocket<TCPOverIPv4OverEthernetAdapter>;

                          using LossyTCPOverUDPSpongeSocket = TCPSpongeSocket<LossyTCPOverUDPSocketAdapter>;
                          using LossyTCPOverIPv4SpongeSocket = TCPSpongeSocket<LossyTCPOverIPv4OverTunFdAdapter>;
                          + +

                          它很完美地以策略模式的形式,凝结出了我们本次实验所需的各种协议栈的共同代码,放进了TCPSpongeSocket,而将涉及到协议栈差异的部分用adapter完成。

                          +

                          TCPSpongeSocket中,adapter主要完成了如下操作:

                          +
                            +
                          1. adapter的tick函数

                            +
                            // in tcp_loop
                            _tcp.value().tick(next_time - base_time);
                            _datagram_adapter.tick(next_time - base_time);
                          2. +
                          3. 作为订阅事件的IO流

                            +
                            _eventloop.add_rule(_datagram_adapter,
                            Direction::In,
                            [&] {
                            // ...
                          4. +
                          5. TCP层通过对其读写来获取TCP segment

                            +
                            auto seg = _datagram_adapter.read();
                            _datagram_adapter.write(_tcp->segments_out().front());
                          6. +
                          7. 记录各类参数

                            +
                            datagram_adapter.config().destination.to_string()
                          8. +
                          +

                          Inheritance graph

                          +

                          具体实现说实话没什么好说的,确实无非也就是上面那几个方法,然后在里面包装下和操作系统提供的tun和tap的接口交互罢了,代码也比较简单,此处就不说了。

                          +

                          apps

                          除了对协议栈的实现之外,在app文件夹下还有许多对我们实现的协议栈的应用实例。我认为了解下应用实例也是很重要的。

                          +

                          bidirectional_stream_copy

                          其作用就是建立stdin/stdout与socket的关联。它从stdin读输入,作为上层app的输入写入socket;从socket读输出,传给上层app,也即stdout输出。它的具体实现在stdin/stdout之间隔了两条bytestream,分别是_inbound_outbound

                          +

                          由于stdin、stdout、socket本质上都是fd,所以我们依然可以采用跟上面一样的事件驱动方式。我们只需在socket有输出时马上读给inbound bytestream,在inbound bytestream有输入时马上读给stdout,在stdin有输入时马上写入outbound bytestream,在outbound bytestream有输入时马上读给socket。遵守这4条rule就行了。

                          +

                          因而,具体实现就是TCPSpongeSocket::_initialize_TCPTCPSpongeSocket::_tcp_loop的结合体,订阅事件+循环等待。由于跟前面类似,在此就不放代码了。

                          +

                          其他

                          其他都太复杂了,感觉我水平一般还不大能理解,也懒得看了【草】总之先咕咕咕

                          +]]> +
                          + + 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. type

                            -

                            好像比如说取SOCK_DGRAM就是UDP,取SOCK_STREAM就是TCP。

                            +
                          4. buffer进行第二轮扫描

                            +

                            如果node与buffer[i]产生重叠( (it->left >= left && it->left <= right)),那么更新node为node∪buffer[i],并且将buffer[i]从buffer中删去。

                          -
                          代码
                          /* 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();
                          }
                          // ...
                          +

                          我们在合并区间时,不仅需要对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);
                          -

                          * 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.
                          +

                          写入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);
                          }
                          }
                          -

                          事件监听

                          完成事件监听的核心部分是方法_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(); });
                          }
                          +

                          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;
                          }
                          -
                          _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;
                          }
                          }
                          }
                          +

                          但是这样很明显有两个问题。

                          +

                          一是就算你超过了,你也不能直接丢弃掉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。尽可能多地写入,把写入不了的部分丢弃。
                          6. +
                          +
                          例子

                          下面举个例子来说明整个流程。

                          +
                          initial:  
                          buffer = { [1,3], [5,8],[10,12],[13,14] } , capacity = 12, remaining = 12-9=3, data = [2,11], start = 2
                          -
                          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);
                          +

                          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
                          -

                          事件类型events可以为下列值:

                          -
                          POLLIN:有数据可读
                          POLLRDNORM:有普通数据可读,等效于POLLIN
                          POLLRDBAND:有优先数据可读
                          POLLPRI:有紧迫数据可读
                          POLLOUT:写数据不会导致阻塞
                          POLLWRNORM:写普通数据不会导致阻塞
                          POLLWRBAND:写优先数据不会导致阻塞
                          POLLMSG:SIGPOLL消息可用
                          POLLER:指定的文件描述符发生错误
                          POLLHUP:指定的文件描述符挂起事件
                          POLLNVAL:无效的请求,打不开指定的文件描述符
                          -
                          -

                          我们在前面的eventloop的rule初始化中:

                          -
                          _eventloop.add_rule(_datagram_adapter,
                          Direction::In,
                          [&] { ... });
                          +

                          在循环结束的这里已经处理:

                          +
                          if (remaining < right - start) {
                          right = remaining + start;
                          if (eof == 1)
                          is_eof = false;
                          }
                          -

                          这个的意思是针对_datagram_adapter这个文件的Direction::In这个事件发生时,就会执行[&]中的事件。那么Direction::In是什么?

                          -
                          enum class Direction : short {
                          In = POLLIN, //!< Callback will be triggered when Rule::fd is readable.
                          Out = POLLOUT //!< Callback will be triggered when Rule::fd is writable.
                          };
                          +

                          */

                          +

                          然后扫描到[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
                          // ...
                          }
                          -

                          可见,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);
                          }
                          +

                          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;
                          // ...
                          }
                          }
                          -
                          _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;
                          }
                          }
                          +
                          小细节

                          因而,在一开始发现remaining满的时候,不能直接goto end。

                          +
                          // if(remaining == 0)	goto end; // 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能不能腾空间。

                          +

                          eof

                          对于eof的处理也是需要注意的一个小细节。

                          +

                          我们不能这么写:

                          +
                          if (eof)
                          _output.end_input();
                          -

                          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>;
                          +

                          原因有二,一是最后一个数据包有可能是部分丢失,二是整流可能还未结束。

                          +

                          所以我们应该在成员变量中维护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; }
                          -

                          它很完美地以策略模式的形式,凝结出了我们本次实验所需的各种协议栈的共同代码,放进了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的结合体,订阅事件+循环等待。由于跟前面类似,在此就不放代码了。

                          -

                          其他

                          其他都太复杂了,感觉我水平一般还不大能理解,也懒得看了【草】总之先咕咕咕

                          ]]>
                          @@ -7182,161 +7116,120 @@ url访问填写http://localhost/webdemo4_war/*.do

                          在学习本章内容之前,我特地先去回顾了下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();
                          }
                          -]]> -
                          - - Lab3 TCPSender - /2023/02/25/cs144$lab3/ - Lab3 TCPSender

                          前置知识

                          在TCP协议中,TCPSender负责对ack进行处理,将字节流封装为TCP报文,根据拥塞窗口的大小传输数据,以及管理超时重传。

                          -

                          我们的TCPSender需要做的是:

                          -
                            -
                          1. 维护拥塞窗口

                            -

                            image-20230228105405827

                            -

                            我们需要通过ackno和window_size两个参数维护拥塞窗口的大小

                            -
                          2. -
                          3. 填充拥塞窗口

                            -

                            必须as possible。除非拥塞窗口满或者ByteStream空才不填。

                            -

                            对于从ByteStream读出的数据,我们需要把其封装为一个TCPSegment再向_segment_out输出

                            -
                          4. -
                          5. 记录哪一部分ack了,哪一部分没有ack

                            -

                            我们需要在发送segment的同时暂存segment,当且仅当接收到ack,并且ack为segment.seqno+length的时候才能将其释放。

                            -
                          6. -
                          7. 管理超时重传

                            -

                            当对方超过一段时间还没有收到数据时,需要进行超时重传

                            -

                            以segment为单位,一个segment重传具有原子性。

                            -

                            在sender和暂存segment的数据结构中保存时钟滴答

                            -
                          8. -
                          -

                          特别的,指导书上有一段话表述得很有意思:

                          -

                          image-20230228110046088

                          -

                          这体现了TCPReceiverTCPSender之间的对偶关系,这种细节性的设计理念值得学习。

                          -

                          感想

                          写完TCPSender后我还是觉得有些迷茫……就跟TCPReceiver一样。说不出来具体是哪里不清楚,但总感觉隐隐约约有些怪怪的?总感觉相互之间接口有点混乱,对它们之间是怎么交互的一概不知。我想这是由于我们是自底向上实现TCP协议所带来的问题。希望这种感觉在实现完TCPConnection之后可以好转吧。

                          -

                          TCPReceiver的主要任务是把segment拼接成字节流,以及维护即将要告知TCPSender的ackno和拥塞窗口大小。而TCPSender的作用就是把字节流切成segment,并且根据ackno和拥塞窗口大小,进行数据的填充以及超时重传的管理。可以看到,它们是对偶的关系。

                          -

                          初见思路

                          看完指导书以及各种接口定义可以得知,我们需要:

                          -
                            -
                          1. 增加成员变量

                            -
                              -
                            1. window_size 拥塞窗口的大小

                              -
                            2. -
                            3. ackono 记录当前收到的最大ackno

                              -
                            4. -
                            5. ticks 记录sender从出生到现在的时钟滴答

                              -
                            6. -
                            7. tmp_size 记录tmp_segments 中的数据字节数(注意算上SYN和FIN)

                              -
                            8. -
                            9. tmp_segments 暂存segment,等待收到ack

                              -

                              数据结构:

                              -

                              list,自定义struct,结构体内有

                              -
                                -
                              • TCPSegment
                              • -
                              • seqno 记录该segment的起始数据的seq
                              • -
                              • data_size 记录该segment携带数据的长度
                              • -
                              -
                            10. -
                            11. cons_retran 记录连续的超时重传次数

                              -
                            12. -
                            13. syn 标记当前是否为第一个segment

                              -
                            14. -
                            15. fin

                              -
                            16. -
                            17. rto 记录当前的RTO

                              -
                            18. -
                            19. timer_start 记录timer是否等待中

                              -
                            20. -
                            21. timer_ticks 记录timer开启时的时间

                              -
                            22. -
                            -
                          2. -
                          3. 实现一个定时函数

                            -

                            第一次从bytestream取出数据包装为segment的时候(也即发送SYN报文)开启它,当所有data都收到ack的时候(也即FIN报文也被成功ACK)关闭它

                            -

                            应该在ticks中被调用

                            -
                            -

                            Every time a segment containing data (nonzero length in sequence space) is sent (whether it’s the first time or a retransmission), if the timer is not running, start it running so that it will expire after RTO milliseconds.

                            -
                            -

                            当timer触发时,我们需要重传tmp_segments 队列头。

                            -

                            如果空间足够,直接重传就行了,然后double RTO,然后用RTO reset timer,然后再次启动timer。

                            -

                            如果空间不足够,只做上面那个的后两步,也即reset timer,然后再次启动timer。

                            -
                          4. -
                          5. ack_received

                            +

                            思路

                            基本流程

                            得益于Lab1那个复杂算法的健壮性和多功能性,我们对TCPReceiver的实现就可以变得更加简洁。我们不再需要关心报文是否能够被成功接收、报文是否重叠等等等。我们仅需对SYN和FIN这样的报文做特殊的参数处理,将seqno转化为index,然后直接传入我们的StreamReassembler中就行了。

                            +

                            也即,基本流程为:

                              -
                            1. 更新window_size和ackno

                              -
                            2. -
                            3. 重置超时重传

                              -

                              如果接收到的ackno比以前的大,则重置RTO,重启timer(如果tmp_segments不为空),重置cons_retran

                              -
                            4. -
                            5. 从tmp_segments中删除元素

                              -
                            6. -
                            7. 调用fill_window

                              -
                            8. +
                            9. 如果收到SYN报文,则对一些参数进行初始化,并且标记数据传输开始信号syn为true
                            10. +
                            11. 如果syn为true,则计算index后传入整流器
                            12. +
                            13. 判断是否需要加上FIN报文的比特位
                            -
                          6. -
                          7. fill_window

                            -

                            如果window_size - tmp_size <= 0 或者 byte stream空,则什么也不做

                            -

                            否则根据syn和fin标记创建一个new segment,然后写入out stream

                            +
                            一些细节
                            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的定义

                            我现在还是搞不懂这东西究竟是什么玩意……

                            +

                            指导书上是这么说的:

                            -

                            no bigger than the value given by TCPConfig::MAX PAYLOAD SIZE (1452 bytes)

                            +

                            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();
                            }
                            +]]> + + + Lab0 + /2023/02/25/cs144$lab0/ + Lab0
                            +

                            本次实验一直在强调的一点就是,TCP的功能是将底层的零散数据包,拼接成一个reliable in-order的byte stream。这个对我来说非常“振聋发聩”(夸张了233),以前只是背诵地知道TCP的可靠性,这次我算是第一次知道了所谓“可靠”究竟可靠在哪:一是保证了序列有序性,二是保证了数据不丢失(从软件层面)。

                            +

                            还有一个就是大致了解了cs144的主题:实现TCP协议。也就是说,运输层下面的那些层是不用管的吗?不过这样也挺恰好,我正好在学校的实验做过对下面这些层的实现了,就差一个TCP23333这样一来,我的协议栈就可以完整了。

                            -

                            If the receiver has announced a window size of zero, the fifill window method should act like the window size is one.

                            +

                            本次实验与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++;
                            }
                            -
                          8. -
                          -

                          细节补充

                          实现起来虽然很复杂,但思路确实很简单,正确思路和初见思路差不多,指导书写得很好很详细【以至于一开始我被指导书这么多内容给吓到了】。在这里只记录点实现过程中遇到的一些小错误以及我各个部分的实现细节补充。

                          -

                          timer实现

                          指导书的建议是实现一个类,但是我太懒了()而且确实这个timer的状态也很少,因而我就直接把它写在sender里面了。

                          -

                          SYN报文是否可以带数据

                          此实验未涉及这个。本次全部的测试用例都是SYN报文不携带数据的情况。【因为发出syn报文之后才将window_size设置为非0情况】

                          -

                          如果需要SYN报文不携带数据,可以在fill_window中把这句话:

                          -
                          if (!(_stream.buffer_empty() || remaining == 0)) {
                          +

                          Fetch a Web page

                          主要是介绍了telnet指令

                          +

                          屏幕截图 2023-02-23 194758

                          +

                          Send yourself an email

                          用的是telnet带smtp参

                          +

                          Listening and connecting

                          上面的telnet是一个client program。接下来我们要把自己放在server的位置上。

                          +

                          用的是netcat指令。

                          +

                          image-20230223202202509

                          +

                          Use socket to write webget

                          这个确实不难,就是这个地方有点坑:

                          +
                          +

                          Please note that in HTTP, each line must be ended with “\r\n” (it’s not sufficient to use just “\n” or endl).

                          +
                          +

                          导致我跟400 Bad Request大眼瞪小眼了好久。。。

                          +
                          void get_URL(const string &host, const string &path) {
                          TCPSocket sock;
                          string tmp;
                          // sock.set_blocking(true);// 默认情况下即为true
                          sock.connect(Address(host,"http"));
                          sock.write("GET " + path + " HTTP/1.1\r\nHost: " +
                          host + "\r\nConnection: close\r\n\r\n");

                          while((tmp = sock.read(1)) != ""){
                          cout << tmp;
                          }
                          /*
                          上面那个写法不大规范,更规范的写法:
                          while(!sock.eof()){
                          cout << sock.read(1);
                          }
                          */
                          sock.close();
                          }
                          -

                          修改为这句话:

                          -
                          if (!segment.header().syn&&!(_stream.buffer_empty() || remaining == 0)) {
                          +

                          还有一点值得注意的是,当我这样时:

                          +
                          TCPSocket sock;
                          sock.set_blocking(false);
                          sock.connect(Address(host,"http"));
                          -

                          代码

                          头文件

                          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; }
                          +

                          会报错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 ++;
                          }
                          -

                          具体实现

                          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);
                          }
                          ]]>
                          +

                          结果就是测试用例Timeout。我找了很久都不知道错在了哪,最后求助了场外观众【罪过……这次实验太不独立了】,学着他把length改成了这样:

                          +
                          int length = min(data.length(),capacity-buffer.size());
                          + +

                          发现成了。

                          +

                          我去看了看testbench,猜测应该是因为阻塞了,我还以为是deque自身会阻塞【是的,我完全没注意到自己顺手把阻塞写了下去】,查了半天发现不会,最后才发现是自己不小心搞错了呃呃…………

                          +

                          代码

                          头文件声明

                          class ByteStream {
                          private:
                          // Your code here -- add private members as necessary.

                          // Hint: This doesn't need to be a sophisticated data structure at
                          // all, but if any of your tests are taking longer than a second,
                          // that's a sign that you probably want to keep exploring
                          // different approaches.

                          size_t total_write;
                          size_t total_read;
                          bool is_input_end;
                          const size_t capacity;
                          deque<char> buffer;
                          + +

                          具体实现

                          ByteStream::ByteStream(const size_t cap) : total_write(0),total_read(0),is_input_end(false),capacity(cap),buffer(){ }

                          //! Write a string of bytes into the stream. Write as many
                          //! as will fit, and return how many were written.
                          //! \returns the number of bytes accepted into the stream
                          size_t ByteStream::write(const string &data) {
                          if(is_input_end == true) is_input_end = false;
                          int pointer = 0;
                          int length = data.length();
                          while(is_input_end == false&&pointer<length){
                          if(buffer.size() == capacity) break;
                          buffer.push_back(data[pointer]);
                          pointer++;
                          }
                          total_write+=pointer;
                          return pointer;
                          }
                          //! Peek at next "len" bytes of the stream
                          //! \param[in] len bytes will be copied from the output side of the buffer
                          string ByteStream::peek_output(const size_t len) const {
                          string res;
                          size_t i = 0;
                          for (auto it = buffer.begin(); it != buffer.end(); it++) {
                          if (i >= len)
                          break;
                          i++;
                          res.push_back(*it);
                          }
                          return res;
                          }

                          //! Remove bytes from the buffer
                          //! \param[in] len bytes will be removed from the output side of the buffer
                          void ByteStream::pop_output(const size_t len) {
                          size_t i;
                          for (i = 0; i < len; i++) {
                          if (buffer.empty())
                          break;
                          buffer.pop_front();
                          }
                          total_read+=i;
                          }

                          //! Read (i.e., copy and then pop) the next "len" bytes of the stream
                          //! \param[in] len bytes will be popped and returned
                          //! \returns a string
                          std::string ByteStream::read(const size_t len) {
                          string res = peek_output(len);
                          pop_output(len);
                          return res;
                          }

                          void ByteStream::end_input() {is_input_end = true;}

                          bool ByteStream::input_ended() const { return is_input_end; }

                          size_t ByteStream::buffer_size() const { return buffer.size(); }

                          bool ByteStream::buffer_empty() const { return buffer.empty(); }

                          bool ByteStream::eof() const { return is_input_end && buffer.empty(); }

                          size_t ByteStream::bytes_written() const { return total_write; }

                          size_t ByteStream::bytes_read() const { return total_read; }

                          size_t ByteStream::remaining_capacity() const { return capacity - buffer.size(); }
                          +]]>
                          Lab4 TCPConnection @@ -7487,49 +7380,159 @@ url访问填写http://localhost/webdemo4_war/*.do

                          t_udp_client_send超时

                          image-20230304215748823

                          这个错因非常地诡异,我到最后也还是没有自己找出来。直到我瞎搜来搜去看到了这篇文章:

                          -

                          我也是真的很佩服这篇文章的作者能找到这个点

                          -

                          image-20230304221420748

                          -

                          https://www.cnblogs.com/lawliet12/p/17066719.html

                          +

                          我也是真的很佩服这篇文章的作者能找到这个点

                          +

                          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……

                          +

                          改了之后立刻所有测试都能跑通了【悲】

                          +
                          +

                          那么问题来了,为什么错误版本就会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(经测试是这样的)。这非常奇怪,我也不知道为什么。

                          +
                          +

                          代码

                          【珍贵的调试用代码没删的版本放在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();
                          // ...
                          + +

                          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;
                          }
                          }
                          + +

                          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;
                          }
                          + +

                          in libsponge/tcp_helper/fd_adapter.cc

                          +

                          image-20230304172207234

                          +]]> +
                          + + Lab3 TCPSender + /2023/02/25/cs144$lab3/ + Lab3 TCPSender

                          前置知识

                          在TCP协议中,TCPSender负责对ack进行处理,将字节流封装为TCP报文,根据拥塞窗口的大小传输数据,以及管理超时重传。

                          +

                          我们的TCPSender需要做的是:

                          +
                            +
                          1. 维护拥塞窗口

                            +

                            image-20230228105405827

                            +

                            我们需要通过ackno和window_size两个参数维护拥塞窗口的大小

                            +
                          2. +
                          3. 填充拥塞窗口

                            +

                            必须as possible。除非拥塞窗口满或者ByteStream空才不填。

                            +

                            对于从ByteStream读出的数据,我们需要把其封装为一个TCPSegment再向_segment_out输出

                            +
                          4. +
                          5. 记录哪一部分ack了,哪一部分没有ack

                            +

                            我们需要在发送segment的同时暂存segment,当且仅当接收到ack,并且ack为segment.seqno+length的时候才能将其释放。

                            +
                          6. +
                          7. 管理超时重传

                            +

                            当对方超过一段时间还没有收到数据时,需要进行超时重传

                            +

                            以segment为单位,一个segment重传具有原子性。

                            +

                            在sender和暂存segment的数据结构中保存时钟滴答

                            +
                          8. +
                          +

                          特别的,指导书上有一段话表述得很有意思:

                          +

                          image-20230228110046088

                          +

                          这体现了TCPReceiverTCPSender之间的对偶关系,这种细节性的设计理念值得学习。

                          +

                          感想

                          写完TCPSender后我还是觉得有些迷茫……就跟TCPReceiver一样。说不出来具体是哪里不清楚,但总感觉隐隐约约有些怪怪的?总感觉相互之间接口有点混乱,对它们之间是怎么交互的一概不知。我想这是由于我们是自底向上实现TCP协议所带来的问题。希望这种感觉在实现完TCPConnection之后可以好转吧。

                          +

                          TCPReceiver的主要任务是把segment拼接成字节流,以及维护即将要告知TCPSender的ackno和拥塞窗口大小。而TCPSender的作用就是把字节流切成segment,并且根据ackno和拥塞窗口大小,进行数据的填充以及超时重传的管理。可以看到,它们是对偶的关系。

                          +

                          初见思路

                          看完指导书以及各种接口定义可以得知,我们需要:

                          +
                            +
                          1. 增加成员变量

                            +
                              +
                            1. window_size 拥塞窗口的大小

                              +
                            2. +
                            3. ackono 记录当前收到的最大ackno

                              +
                            4. +
                            5. ticks 记录sender从出生到现在的时钟滴答

                              +
                            6. +
                            7. tmp_size 记录tmp_segments 中的数据字节数(注意算上SYN和FIN)

                              +
                            8. +
                            9. tmp_segments 暂存segment,等待收到ack

                              +

                              数据结构:

                              +

                              list,自定义struct,结构体内有

                              +
                                +
                              • TCPSegment
                              • +
                              • seqno 记录该segment的起始数据的seq
                              • +
                              • data_size 记录该segment携带数据的长度
                              • +
                              +
                            10. +
                            11. cons_retran 记录连续的超时重传次数

                              +
                            12. +
                            13. syn 标记当前是否为第一个segment

                              +
                            14. +
                            15. fin

                              +
                            16. +
                            17. rto 记录当前的RTO

                              +
                            18. +
                            19. timer_start 记录timer是否等待中

                              +
                            20. +
                            21. timer_ticks 记录timer开启时的时间

                              +
                            22. +
                            +
                          2. +
                          3. 实现一个定时函数

                            +

                            第一次从bytestream取出数据包装为segment的时候(也即发送SYN报文)开启它,当所有data都收到ack的时候(也即FIN报文也被成功ACK)关闭它

                            +

                            应该在ticks中被调用

                            +
                            +

                            Every time a segment containing data (nonzero length in sequence space) is sent (whether it’s the first time or a retransmission), if the timer is not running, start it running so that it will expire after RTO milliseconds.

                            +
                            +

                            当timer触发时,我们需要重传tmp_segments 队列头。

                            +

                            如果空间足够,直接重传就行了,然后double RTO,然后用RTO reset timer,然后再次启动timer。

                            +

                            如果空间不足够,只做上面那个的后两步,也即reset timer,然后再次启动timer。

                            +
                          4. +
                          5. ack_received

                            +
                              +
                            1. 更新window_size和ackno

                              +
                            2. +
                            3. 重置超时重传

                              +

                              如果接收到的ackno比以前的大,则重置RTO,重启timer(如果tmp_segments不为空),重置cons_retran

                              +
                            4. +
                            5. 从tmp_segments中删除元素

                              +
                            6. +
                            7. 调用fill_window

                              +
                            8. +
                            +
                          6. +
                          7. fill_window

                            +

                            如果window_size - tmp_size <= 0 或者 byte stream空,则什么也不做

                            +

                            否则根据syn和fin标记创建一个new segment,然后写入out stream

                            +
                            +

                            no bigger than the value given by TCPConfig::MAX PAYLOAD SIZE (1452 bytes)

                            -

                            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();
                            // ...
                            +
                          8. +
                          +

                          细节补充

                          实现起来虽然很复杂,但思路确实很简单,正确思路和初见思路差不多,指导书写得很好很详细【以至于一开始我被指导书这么多内容给吓到了】。在这里只记录点实现过程中遇到的一些小错误以及我各个部分的实现细节补充。

                          +

                          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 @@ -7677,6 +7680,30 @@ url访问填写http://localhost/webdemo4_war/*.do

                          代码

                          头文件

                          class NetworkInterface {
                          private:
                          // ...

                          // arp映射的元素定义
                          struct arp_node {
                          EthernetAddress mac;// ip地址key对应的mac地址
                          size_t ticks;// 该条记录被记录时的时间
                          };
                          // ARP映射,<IP地址,元素结点>
                          std::map<uint32_t, arp_node> arp_mappings{};

                          struct waiting_node {
                          std::vector<EthernetFrame> frames{};// 等待在该IP地址的以太帧们
                          size_t latest_ticks = 0; // 等候中的frame们最晚放入者的时间
                          };
                          // 等待队列,<IP地址,元素结点>
                          std::map<uint32_t, waiting_node> waiting_frames{};

                          size_t ticks = 0;// 出生到现在经过的时钟滴答
                          // 自定义方法,用于发送一个arp报文【请求/响应】
                          void send_arp(uint32_t target_ip, EthernetAddress eth_add, uint16_t opcode);

                          具体实现

                          #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 @@ -7719,33 +7746,193 @@ url访问填写http://localhost/webdemo4_war/*.do

                          至此以来,我们的协议栈才算真正完整了。

                          Lab0

                          Lab1 StreamReassembler

                          Lab2 TCPReceiver

                          Lab3 TCPSender

                          Lab4 TCPConnection

                          Lab5 NetworkInterface

                          Lab6 Router

                          其他的对实验未涉及的思考

                          ]]> - labs + labs + +
                          + + 驱动开发小记 + /2023/10/27/driver_develop/ + 此为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 等)。

                          +
                          +

                          https://blog.csdn.net/m0_56694518/article/details/131207367

                          +

                          GPIO是英文General Purpose Input/Output的缩写,中文翻译为通用输入/输出。它是一种在数字电子系统中常见的接口类型,用于与外部设备进行通信和控制。

                          +

                          GPIO接口可以作为输入或输出引脚使用。作为输入引脚时,GPIO可以接收来自外部设备的电信号,并将其转换为数字信号,供系统内部使用。作为输出引脚时,GPIO可以将数字信号发送到外部设备,从而实现对其的控制。

                          +
                            +
                          1. 引脚:GPIO接口通常由一组引脚组成,每个引脚都可以用作输入或输出。每个引脚都有一个唯一的标识符,如GPIO0、GPIO1等。

                            +
                          2. +
                          3. 输入模式:当GPIO引脚配置为输入模式时,它可以接收外部设备发送的电信号。通常,输入引脚可以读取高电平(1)或低电平(0)状态,或者在某些系统中可以读取模拟信号。

                            +
                          4. +
                          5. 输出模式:当GPIO引脚配置为输出模式时,它可以向外部设备发送数字信号。输出引脚可以设置为高电平(1)或低电平(0),以控制连接的设备的状态。

                            +
                          6. +
                          7. 状态和电平:GPIO引脚的状态表示当前引脚的输入或输出电平。高电平通常表示逻辑1,低电平表示逻辑0。在某些系统中,还可以使用其他状态,如浮空、上拉和下拉等。

                            +
                          8. +
                          9. 控制寄存器:为了配置和控制GPIO引脚的功能,通常需要通过写入特定的控制寄存器来设置引脚的模式、状态和电平。这些寄存器的具体配置取决于所使用的硬件平台和操作系统。【这个估计就是我们驱动要干的】

                            +
                          10. +
                          +
                          +

                          也就是意思就是,这个gpio相当于一个接口,能够把系统的信号传给硬件,也能把硬件信号传给系统。所以有时候我们可以通过读写其寄存器来与硬件交互。比如, GPIO每个引脚连接着一个led,那么我们通过读写gpio的控制寄存器,控制每个引脚输出1/0,就可以控制led灯亮or灭。

                          +
                          +

                          GPIO的实际应用非常广泛,以下是一些常见的示例:

                          +
                            +
                          1. 控制LED:将GPIO引脚配置为输出模式,可以通过设置引脚的高低电平状态来控制LED的亮灭。

                            +
                          2. +
                          3. 按钮输入:将GPIO引脚配置为输入模式,可以连接按钮或开关,并通过读取引脚的电平状态来检测按钮是否被按下或开关是否打开。

                            +
                          4. +
                          5. 传感器接口:通过GPIO引脚,可以连接各种传感器,如温度传感器、湿度传感器、光照传感器等。传感器的输出信号可以通过读取GPIO引脚的状态来获取。

                            +
                          6. +
                          7. 驱动电机:通过GPIO引脚,可以连接电机驱动器,并通过设置引脚的高低电平状态来控制电机的运行方向和速度。

                            +
                          8. +
                          9. 与外部设备通信:通过GPIO引脚,可以与其他外部设备进行通信,如显示器、LCD屏幕、数码管等。通过设置引脚的状态和电平,可以发送数据或控制命令

                            +
                          10. +
                          11. 脉冲宽度调制(PWM)输出:一些GPIO引脚支持PWM功能,可以生成模拟信号,用于控制电机速度、调节LED亮度等需要模拟输出的应用。

                            +
                          12. +
                          13. 扩展IO功能:通过使用扩展芯片或GPIO扩展板,可以增加系统的GPIO引脚数量,从而实现更多外部设备的控制和通信。

                            +
                          14. +
                          +
                          +

                          寄存器

                          +

                          4 个 32 位 配 置 寄 存 器

                          +
                            +
                          1. GPIOx_MODER 模式寄存器

                            +

                            用于配置GPIO引脚的模式(输入或输出)。每个引脚通常使用两个位表示模式,例如00表示输入模式,01表示输出模式。

                            +
                          2. +
                          3. GPIOx_OTYPER 输出模式寄存器

                            +

                            用于配置GPIO引脚的输出类型。每个引脚通常使用一个位表示输出类型,例如0表示推挽输出,1表示开漏输出。

                            +
                          4. +
                          5. GPIOx_ OSPEEDR 输出速度寄存器

                            +

                            用于配置GPIO引脚的输出速度。每个引脚通常使用两个位表示输出速度,例如00表示低速,11表示高速。

                            +
                          6. +
                          7. GPIOx_PUPDR 上拉下拉寄存器

                            +

                            用于配置GPIO引脚的上拉或下拉电阻。每个引脚通常使用两个位表示上拉/下拉配置,例如00表示无上拉/下拉,01表示上拉,10表示下拉。

                            +
                          8. +
                          +

                          2 个 32 位数据寄存器
                          GPIOx_IDR 输入数据寄存器
                          GPIOx_ODR 输出数据寄存器

                          +

                          每个位对应一个引脚,读取/写入该位。

                          +

                          1个 32 位置位 / 复位寄存器
                          GPIOx_BSRR 置位 / 复位寄存器

                          +

                          用于通过设置或复位位来控制GPIO引脚的输出电平。每个引脚通常使用两个位,一个位用于置位(设置为1),另一个位用于复位(设置为0)。

                          +

                          2 个 32 位复用功能寄存器
                          GPIOx_AFRH
                          GPIOx_AFRL

                          +

                          用于配置GPIO引脚的复用功能,例如将引脚用作特定的外设功能(如UART、SPI等)。这些寄存器通常将32位分为两个部分,每个部分对应一组引脚。

                          +
                          +

                          模式

                          +

                          https://zhuanlan.zhihu.com/p/612333717?utm_id=0

                          +
                          +

                          v2-9ece20d4c5fb58bd9c9736a14e952403_1440w

                          +

                          原来上下拉是这个意思啊,就是缺省值呗。

                          +

                          一般来说,开漏输出连接上拉输入或浮空输入的外部元件,推挽输出连接下拉输入的外部元件。

                          +

                          当引脚具有复用功能时,它可以在不同的工作模式下切换为不同的功能,而无需切换整个 GPIO 模式。

                          +

                          找到测试程序

                          感觉其实思路还是比较清晰。看这个:

                          +
                          +

                          https://milkv.io/zh/docs/duo/application-development/wiringx

                          +

                          https://github.com/milkv-duo/duo-examples/blob/main/README-zh.md

                          +
                          +

                          也即我最后gpio的效果就是能够运行它给的这个blink example就行。等下回去试下用linux和rtt试试。

                          +
                          int gpio_test(void)
                          {
                          int DUO_LED = 25;

                          rt_pin_mode(DUO_LED, PIN_MODE_OUTPUT);
                          rt_pin_write(DUO_LED, PIN_LOW);

                          for (int i = 0; i < 10; i ++) { // 闪烁十次
                          rt_kprintf("Duo LED GPIO (wiringX) %d: High\n", DUO_LED);
                          rt_pin_write(DUO_LED, PIN_HIGH);
                          rt_thread_delay(RT_TICK_PER_SECOND);
                          rt_kprintf("Duo LED GPIO (wiringX) %d: Low\n", DUO_LED);
                          rt_pin_write(DUO_LED, PIN_LOW);
                          rt_thread_delay(RT_TICK_PER_SECOND);
                          }

                          return 0;
                          }
                          + +

                          总之明天试着开始写吧,我也不知道怎么办了该。可以先问下老师确认led是哪个。我看文档写的GPIOC24但是压根没那东西。

                          +

                          10.14—找到型号

                          感觉可以从相同型号的gpio入手。查了下compatible,这东西好像是什么海思研发的什么the synopsys DW gpio。

                          +
                          +

                          /home/xiunian/rt-thread/milkv-duo-buildroot-sdk/linux_5.10/drivers/gpio/gpio-dwapb.c

                          +

                          https://lkml.org/lkml/2020/8/22/19 commit patch

                          +

                          Milk-V Duo开发板免费体验 GPIO分析

                          +

                          DesignWare_APB_GPIO模块DUT&Testbench仿真

                          +

                          RK3399之8250串口驱动

                          +

                          linux驱动 内核层适配485驱动控制引脚

                          +
                          +

                          我现在发现了milkv的gpio型号是dwapb,找到了它对应的驱动手册【DW_apb_gpio_databook 浅看了下,里面至少有介绍寄存器是在干什么】,还有linux【drivers/gpio/gpio-dwapb.c drivers/gpio/gpio-pl061.c】和rtt【bsp/ft2004/libraries/bsp/ft_gpio/ft_gpio.c】同一型号的驱动代码,我准备对照着这几个参考弄下

                          +

                          然后各个回调的介绍可以看documentation/device/pin/pin.md

                          +

                          总之,不就是看寄存器都是啥东西,然后对应实现read write嘛!概念上是不难的,好好研究,相信可以!

                          +

                          接下来的思路就是,看下那几个寄存器具体的description,理解下两个参考的代码,就可以慢慢开始写了。测试可能需要再花点心思因为毕竟那个还跑步起来。。。总之方向算是比较明确了(至少比前几天明确)加油捏。

                          +

                          10.15—寄存器了解

                          这东西甚至是一个bit一个bit控制的

                          +
                          #define GPIO_SWPORTA_DR		0x00 //A 组端口输出寄存器
                          写入该寄存器的值独立控制端口A中对应数据位的方向。0是input,1是output
                          #define GPIO_SWPORTA_DDR 0x04 //A 组端口方向控制寄存器
                          #define GPIO_SWPORTB_DR 0x0c //B 组端口输出寄存器
                          #define GPIO_SWPORTB_DDR 0x10 //B 组端口方向控制寄存器
                          #define GPIO_SWPORTC_DR 0x18 //C 组端口输出寄存器
                          #define GPIO_SWPORTC_DDR 0x1c //C 组端口方向控制寄存器
                          #define GPIO_SWPORTD_DR 0x24 //D 组端口输出寄存器
                          #define GPIO_SWPORTD_DDR 0x28 //D 组端口方向控制寄存器

                          1. 这个东西可以用来控制每个bit的开关中断
                          2. 默认关中断
                          3. Output时关中断
                          #define GPIO_INTEN 0x30 //A 组端口中断使能寄存器
                          mask为1表示屏蔽
                          #define GPIO_INTMASK 0x34 //A 组端口中断屏蔽寄存器
                          控制是电平敏感中断还是边缘敏感
                          #define GPIO_INTTYPE_LEVEL 0x38 //A 组端口中断等级寄存器
                          极性
                          #define GPIO_INT_POLARITY 0x3c //A 组端口中断极性寄存器
                          只读
                          #define GPIO_INTSTATUS 0x40 //A 组端口中断状态寄存器
                          是否需要消抖【是不是很熟悉hhh】
                          #define GPIO_PORTA_DEBOUNCE 0x48 //A 组端口防反跳配置寄存器
                          清除A/所有中断
                          #define GPIO_PORTA_EOI 0x4c //A 组端口中断清除寄存器

                          只读,表示A端口连接的数据信号(也即input信号)
                          #define GPIO_EXT_PORTA 0x50 //A 组端口输入寄存器
                          #define GPIO_EXT_PORTB 0x54 //B 组端口输入寄存器
                          #define GPIO_EXT_PORTC 0x58 //C 组端口输入寄存器
                          #define GPIO_EXT_PORTD 0x5c //D 组端口输入寄存器

                          #define DWAPB_DRIVER_NAME "gpio-dwapb"
                          #define DWAPB_MAX_PORTS 4

                          // 步长。可以看到如它所言,确实GPIO_EXT_PORT每个差4,GPIO_SWPORTA_DR和GPIO_SWPORTA_DDR每个差12
                          #define GPIO_EXT_PORT_STRIDE 0x04 /* register stride 32 bits */
                          #define GPIO_SWPORT_DR_STRIDE 0x0c /* register stride 3*32 bits */
                          #define GPIO_SWPORT_DDR_STRIDE 0x0c /* register stride 3*32 bits */

                          #define GPIO_REG_OFFSET_V2 1

                          #define GPIO_INTMASK_V2 0x44
                          #define GPIO_INTTYPE_LEVEL_V2 0x34
                          #define GPIO_INT_POLARITY_V2 0x38
                          #define GPIO_INTSTATUS_V2 0x3c
                          #define GPIO_PORTA_EOI_V2 0x40

                          #define DWAPB_NR_CLOCKS 2

                          #define DWAPB_GPIO_BASE 0x03020000
                          + +

                          难道说,我有一个猜想,就是这个实际表示是说GPIO Port A/C的第16/17、9/10位吗。。。。

                          +

                          image-20231027203332238

                          +

                          所以现在很需要这个pin和port的转换。。。

                          +

                          一个是内核传过来的pin是什么数,还有它上面那个是不是端口和位的意思,这两点需要弄清楚

                          +

                          总之先向来是确认下我的想法:那个图的灰色块和引脚号是同一个东西,GPIOA17这样的字符表示GPIO的A端口寄存器的第16个bit。

                          +

                          如果是这样的话,那么思路就很自然了。我们可以通过图中引脚和GPIO这种的分布,从而得知当前是要写入什么端口什么寄存器,这样就能编写代码了。Good。

                          +

                          10.17—整亮led

                          也许可以搜一下schematic该怎么看,说不定就能知道哪位是引脚号了。

                          +

                          现在先假设GPIO C 24表示GPIO C端口的第24个bit,也即 (1<<23)吧。所以我们在读取和写入的时候,都是先读出原来的值,然后再把对应位设为1or0。但是为啥qemu是一字节一字节写。。。算了相信自己,总之先试试

                          +
                          +

                          然后就去研究了下linux中的gpio……发现了其内部映射。

                          +
                          +
                          ~# cat /sys/class/gpio/gpiochip
                          gpiochip352/ gpiochip384/ gpiochip416/ gpiochip448/ gpiochip480/
                          + +

                          384是GPIOD,416是C,448是B,480是A,每个间距32。而LED对应的是GPIO C 24,所以号是416+24 = 440。

                          +

                          416+24 = 440,也就是说,GPIOC24是port C的第24号,也即第25位。

                          +
                          +

                          然后慢慢整理问题……

                          +
                          +

                          整理一下目前的问题。

                          +

                          首先,我是不是想错了???

                          +

                          按照刚刚在Linux下看到的结果:

                          +
                          ~$ cat /sys/class/gpio/gpiochip
                          gpiochip352/ gpiochip384/ gpiochip416/ gpiochip448/ gpiochip480/
                          + +

                          五个gpio,bank name分别为porte、d、c、b、a。那么问题来了,这个port跟我们在dw_apb手册里看到的gpio的abcd四个port是一个概念吗??

                          +

                          已知,dwapb gpio有ABCD四个Port,因而有四组寄存器。有一点值得注意,到底是一个字节控制一个设备,还是一个bit?

                          +

                          那么我感觉的是,板上有5个gpio,每个gpio都各自有4个port和4K地址空间,每个port都有这样一堆寄存器:

                          +

                          image-20231027203540506

                          +

                          而GPIOC24的意思就是,它是第四个gpio?port夺少?说实话我真的很迷惑。。。

                          +

                          而且我那个写入为什么一点效果没有。。。好痛苦我的天

                          +

                          好,刚刚终于成功把led整亮了。所以是这样的,它那个GPIOABCD并不是一个GPIO的Port ABCD的意思,应该就是纯纯有多个GPIO芯片。然后GPIOC24的意思就是第三块GPIO的A组寄存器第32位,为什么是A不是别的我也不大懂,反正我看了Linux的驱动代码,好像一般也是只用A组端口。

                          +

                          10.18/19—中断

                          很好,目前已经基本完成了数据的读写这部分,接下来要做的就是中断了,中断还是先看看数据手册在做。中断的话,决定先研究下wiringX的那个example,然后来看看中断怎么进行有效性验证。

                          +

                          很好,把测试程序搬了过来,还有学习了下qemu的架构也搬过来了,现在剩下的任务就是看寄存器了

                          +

                          目前是这样,dwapb_pin_attach_irqdwapb_pin_detach_irq负责注册回调函数,dwapb_pin_irq_enable负责开关中断。然后,我们通过rt_hw_interrupt_install(DWAPB_GPIOE_IRQNUM, rt_hw_gpio_isr, &gpio_idx, "gpio");注册中断函数,这样一来每次DWAPB_GPIOE_IRQNUM中断时就会触发该函数,从而执行rt_hw_gpio_isr中遍历所有hdr并执行一遍的流程了。

                          +

                          整理一下它目前需要有哪些寄存器。首先是rt_hw_gpio_isr,需要有一个pending功能;然后是dwapb_pin_attach_irq,需要能设置是电平/边缘触发以及触发的极性;最后是dwapb_pin_irq_enable,需要有个能开关中断的寄存器。

                          +

                          目前是这样,理论上,我通过rt_pin_irq_enable向INTEN写入数据,这b对应的GPIO引脚就会产生一个中断,然后将中断传递给CPU,CPU通过其中断号从而调用中断处理函数,然后就吊我们在it_install中安装的那个回调。然而不知怎的没调。我觉得原因可能两个,一个寄存器看错了,另一个安装没装好。明天对照Linux源码看下吧。

                          +

                          应该不是,还是得靠外界输入才能触发中断。明天问问老师怎么做。

                          +

                          新进展,发现C9C10互连,然后C10间隔输出就行23333这样就可以触发中断,perfect。

                          +

                          不过我目前的问题似乎是,触发了中断之后,不知道为什么没有进入pending处理,导致中断一直未处理从而寄。

                          +

                          牛逼,成了!!!明天改下码风,多写几个测试,应该就结了。好像目前是both有点问题。明天把这个修下。

                          +

                          image-20231027203706048

                          +

                          BOTH_EDGE有点问题,好像那啥玩意没效果,依然保持着上次设置的lowedge。现在去学习下linux的both edge怎么写的。

                          +

                          很好,bug已除。

                          +

                          代码框架

                          GPIO实际上就是一个单纯的在板子和外设之间搬运字节的东西,所以本质上只需做好寄存器配置,以及引脚映射即可。接下来将从几个方面拆解我的代码。

                          +

                          整体架构

                          在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
                          - - 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();
                          }
                          }
                          }
                          ]]>
                          -
                          数据库原理 /2023/11/26/database/ @@ -8440,299 +8627,115 @@ url访问填写http://localhost/webdemo4_war/*.do

                          性质

                          image

                          宏观性

                          image

                          微观性

                          image

                          -

                          特性:ACID

                          image

                          -

                          调度与可串行性

                          概念

                          image

                          -

                          image

                          -

                          image

                          -

                          image

                          -

                          图中最右错误,是因为这样的话B=B-20这个操作事实上就会被覆盖了。这两个事务不关心读的是不是B origin,只需要+10和-20操作都做了就行。

                          -

                          事务模型

                          image

                          -

                          冲突与可串行性

                          -

                          强烈推荐:冲突可串行化、事务优先图。PPT讲的不知道啥玩意

                          -
                          -
                          -

                          也就是说,如果一个schedule经过一系列对任意两个不冲突操作的调换动作后,可以变成一个完全串行的schedule,那么我们就称其为冲突可串行化。

                          -
                          -

                          image

                          -

                          image

                          -

                          image-20231119184216295

                          -

                          image-20231119184114564

                          -

                          image

                          -

                          如果一个 schedule 和某个串行 schedule 冲突等价,则称该 schedule 是冲突可串行化(conflict serializable)的。

                          -

                          image

                          -

                          image

                          -

                          真不懂

                          -

                          image

                          -

                          冲突可串行判别算法

                          image

                          -

                          一种就是上面那个图片例子,一直交换直到得到串行调度;

                          -

                          另一种就是下面要介绍的这个画图了。

                          -

                          说实话真没懂这啥玩意冲突可串行

                          -

                          image

                          -

                          也没搞懂这灰线又tm哪来的。。。

                          -

                          image

                          -

                          基于封锁的并发控制

                          概述

                          image

                          -

                          image

                          -

                          image

                          -

                          image

                          -

                          不满足

                          -

                          考虑因素

                          锁的类型

                          image

                          -

                          封锁协议

                          image

                          -
                          相容性矩阵

                          image

                          -
                          加解锁时机

                          image

                          -
                          封锁粒度

                          image

                          -

                          两段封锁协议

                          两段封锁协议是一种基于锁的并发控制方法。

                          -

                          image

                          -

                          image

                          -

                          满足

                          -

                          image

                          -

                          注意这个两端锁与前面那个只用锁的差距。两段锁把B加锁也移到前面了,所以可以保证冲突可串行性。

                          -

                          image

                          -

                          读nm,不读再见

                          -

                          可以看到,图中确实是满足两段锁协议要求的,加锁段里确实没有解锁段。消除死锁的方式应该就是按照一定顺序获取锁。

                          -

                          image

                          -

                          基于时间戳的并发控制方法

                          概述

                          image

                          -

                          image

                          -

                          简单的调度规则

                          规则

                          image

                          -

                          image

                          -

                          示例

                          image

                          -

                          真没看懂为什么要撤回T2和T3

                          -

                          image

                          -

                          image

                          -

                          冲突是否可串行

                          image

                          -

                          image

                          -

                          另一种调度规则

                          解决问题

                          image

                          -

                          image

                          -

                          image

                          -

                          也就是说U没有提交,所以T要更新的反而也丢了,变得更加过时()

                          -

                          规则

                          image

                          -

                          image

                          -

                          image

                          -

                          image

                          -

                          基于有效性确认的并发控制

                          image

                          -

                          image

                          -

                          image

                          -

                          image

                          -

                          image

                          -

                          image

                          -

                          image

                          -

                          第十二章 数据库事务恢复技术

                          故障类型与影响

                          image

                          -

                          恢复基本思路

                          image

                          -

                          事务故障

                          image

                          -

                          系统故障

                          image

                          -

                          image

                          -

                          image

                          -

                          介质故障

                          image

                          -

                          冗余阵列

                          -

                          image

                          -

                          日志

                          是什么

                          image

                          -

                          image

                          -

                          此时应该上层已经确保了事务的调度是冲突可串行化的,所以这里不用考虑会不会脏读问题。

                          -

                          image

                          -

                          image

                          -

                          Undo型日志

                          记录规则

                          这对应的应该是Steal

                          -

                          image

                          -

                          image

                          -

                          恢复步骤

                          image

                          -

                          检查点

                          image

                          -

                          image

                          -

                          image

                          -

                          Redo型日志

                          记录规则

                          这对应的应该是No Steal

                          -

                          image

                          -

                          image

                          -

                          恢复步骤

                          image

                          -

                          检查点

                          image

                          -

                          结合型日志

                          image

                          -

                          记录规则

                          image

                          -

                          image

                          -

                          恢复步骤

                          image

                          -

                          感觉得先undo再redo?

                          -

                          image

                          -

                          image

                          -]]> -
                          - - 驱动开发小记 - /2023/10/27/driver_develop/ - 此为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 等)。

                          -
                          -

                          https://blog.csdn.net/m0_56694518/article/details/131207367

                          -

                          GPIO是英文General Purpose Input/Output的缩写,中文翻译为通用输入/输出。它是一种在数字电子系统中常见的接口类型,用于与外部设备进行通信和控制。

                          -

                          GPIO接口可以作为输入或输出引脚使用。作为输入引脚时,GPIO可以接收来自外部设备的电信号,并将其转换为数字信号,供系统内部使用。作为输出引脚时,GPIO可以将数字信号发送到外部设备,从而实现对其的控制。

                          -
                            -
                          1. 引脚:GPIO接口通常由一组引脚组成,每个引脚都可以用作输入或输出。每个引脚都有一个唯一的标识符,如GPIO0、GPIO1等。

                            -
                          2. -
                          3. 输入模式:当GPIO引脚配置为输入模式时,它可以接收外部设备发送的电信号。通常,输入引脚可以读取高电平(1)或低电平(0)状态,或者在某些系统中可以读取模拟信号。

                            -
                          4. -
                          5. 输出模式:当GPIO引脚配置为输出模式时,它可以向外部设备发送数字信号。输出引脚可以设置为高电平(1)或低电平(0),以控制连接的设备的状态。

                            -
                          6. -
                          7. 状态和电平:GPIO引脚的状态表示当前引脚的输入或输出电平。高电平通常表示逻辑1,低电平表示逻辑0。在某些系统中,还可以使用其他状态,如浮空、上拉和下拉等。

                            -
                          8. -
                          9. 控制寄存器:为了配置和控制GPIO引脚的功能,通常需要通过写入特定的控制寄存器来设置引脚的模式、状态和电平。这些寄存器的具体配置取决于所使用的硬件平台和操作系统。【这个估计就是我们驱动要干的】

                            -
                          10. -
                          -
                          -

                          也就是意思就是,这个gpio相当于一个接口,能够把系统的信号传给硬件,也能把硬件信号传给系统。所以有时候我们可以通过读写其寄存器来与硬件交互。比如, GPIO每个引脚连接着一个led,那么我们通过读写gpio的控制寄存器,控制每个引脚输出1/0,就可以控制led灯亮or灭。

                          -
                          -

                          GPIO的实际应用非常广泛,以下是一些常见的示例:

                          -
                            -
                          1. 控制LED:将GPIO引脚配置为输出模式,可以通过设置引脚的高低电平状态来控制LED的亮灭。

                            -
                          2. -
                          3. 按钮输入:将GPIO引脚配置为输入模式,可以连接按钮或开关,并通过读取引脚的电平状态来检测按钮是否被按下或开关是否打开。

                            -
                          4. -
                          5. 传感器接口:通过GPIO引脚,可以连接各种传感器,如温度传感器、湿度传感器、光照传感器等。传感器的输出信号可以通过读取GPIO引脚的状态来获取。

                            -
                          6. -
                          7. 驱动电机:通过GPIO引脚,可以连接电机驱动器,并通过设置引脚的高低电平状态来控制电机的运行方向和速度。

                            -
                          8. -
                          9. 与外部设备通信:通过GPIO引脚,可以与其他外部设备进行通信,如显示器、LCD屏幕、数码管等。通过设置引脚的状态和电平,可以发送数据或控制命令

                            -
                          10. -
                          11. 脉冲宽度调制(PWM)输出:一些GPIO引脚支持PWM功能,可以生成模拟信号,用于控制电机速度、调节LED亮度等需要模拟输出的应用。

                            -
                          12. -
                          13. 扩展IO功能:通过使用扩展芯片或GPIO扩展板,可以增加系统的GPIO引脚数量,从而实现更多外部设备的控制和通信。

                            -
                          14. -
                          -
                          -

                          寄存器

                          -

                          4 个 32 位 配 置 寄 存 器

                          -
                            -
                          1. GPIOx_MODER 模式寄存器

                            -

                            用于配置GPIO引脚的模式(输入或输出)。每个引脚通常使用两个位表示模式,例如00表示输入模式,01表示输出模式。

                            -
                          2. -
                          3. GPIOx_OTYPER 输出模式寄存器

                            -

                            用于配置GPIO引脚的输出类型。每个引脚通常使用一个位表示输出类型,例如0表示推挽输出,1表示开漏输出。

                            -
                          4. -
                          5. GPIOx_ OSPEEDR 输出速度寄存器

                            -

                            用于配置GPIO引脚的输出速度。每个引脚通常使用两个位表示输出速度,例如00表示低速,11表示高速。

                            -
                          6. -
                          7. GPIOx_PUPDR 上拉下拉寄存器

                            -

                            用于配置GPIO引脚的上拉或下拉电阻。每个引脚通常使用两个位表示上拉/下拉配置,例如00表示无上拉/下拉,01表示上拉,10表示下拉。

                            -
                          8. -
                          -

                          2 个 32 位数据寄存器
                          GPIOx_IDR 输入数据寄存器
                          GPIOx_ODR 输出数据寄存器

                          -

                          每个位对应一个引脚,读取/写入该位。

                          -

                          1个 32 位置位 / 复位寄存器
                          GPIOx_BSRR 置位 / 复位寄存器

                          -

                          用于通过设置或复位位来控制GPIO引脚的输出电平。每个引脚通常使用两个位,一个位用于置位(设置为1),另一个位用于复位(设置为0)。

                          -

                          2 个 32 位复用功能寄存器
                          GPIOx_AFRH
                          GPIOx_AFRL

                          -

                          用于配置GPIO引脚的复用功能,例如将引脚用作特定的外设功能(如UART、SPI等)。这些寄存器通常将32位分为两个部分,每个部分对应一组引脚。

                          -
                          -

                          模式

                          -

                          https://zhuanlan.zhihu.com/p/612333717?utm_id=0

                          -
                          -

                          v2-9ece20d4c5fb58bd9c9736a14e952403_1440w

                          -

                          原来上下拉是这个意思啊,就是缺省值呗。

                          -

                          一般来说,开漏输出连接上拉输入或浮空输入的外部元件,推挽输出连接下拉输入的外部元件。

                          -

                          当引脚具有复用功能时,它可以在不同的工作模式下切换为不同的功能,而无需切换整个 GPIO 模式。

                          -

                          找到测试程序

                          感觉其实思路还是比较清晰。看这个:

                          -
                          -

                          https://milkv.io/zh/docs/duo/application-development/wiringx

                          -

                          https://github.com/milkv-duo/duo-examples/blob/main/README-zh.md

                          -
                          -

                          也即我最后gpio的效果就是能够运行它给的这个blink example就行。等下回去试下用linux和rtt试试。

                          -
                          int gpio_test(void)
                          {
                          int DUO_LED = 25;

                          rt_pin_mode(DUO_LED, PIN_MODE_OUTPUT);
                          rt_pin_write(DUO_LED, PIN_LOW);

                          for (int i = 0; i < 10; i ++) { // 闪烁十次
                          rt_kprintf("Duo LED GPIO (wiringX) %d: High\n", DUO_LED);
                          rt_pin_write(DUO_LED, PIN_HIGH);
                          rt_thread_delay(RT_TICK_PER_SECOND);
                          rt_kprintf("Duo LED GPIO (wiringX) %d: Low\n", DUO_LED);
                          rt_pin_write(DUO_LED, PIN_LOW);
                          rt_thread_delay(RT_TICK_PER_SECOND);
                          }

                          return 0;
                          }
                          - -

                          总之明天试着开始写吧,我也不知道怎么办了该。可以先问下老师确认led是哪个。我看文档写的GPIOC24但是压根没那东西。

                          -

                          10.14—找到型号

                          感觉可以从相同型号的gpio入手。查了下compatible,这东西好像是什么海思研发的什么the synopsys DW gpio。

                          -
                          -

                          /home/xiunian/rt-thread/milkv-duo-buildroot-sdk/linux_5.10/drivers/gpio/gpio-dwapb.c

                          -

                          https://lkml.org/lkml/2020/8/22/19 commit patch

                          -

                          Milk-V Duo开发板免费体验 GPIO分析

                          -

                          DesignWare_APB_GPIO模块DUT&Testbench仿真

                          -

                          RK3399之8250串口驱动

                          -

                          linux驱动 内核层适配485驱动控制引脚

                          -
                          -

                          我现在发现了milkv的gpio型号是dwapb,找到了它对应的驱动手册【DW_apb_gpio_databook 浅看了下,里面至少有介绍寄存器是在干什么】,还有linux【drivers/gpio/gpio-dwapb.c drivers/gpio/gpio-pl061.c】和rtt【bsp/ft2004/libraries/bsp/ft_gpio/ft_gpio.c】同一型号的驱动代码,我准备对照着这几个参考弄下

                          -

                          然后各个回调的介绍可以看documentation/device/pin/pin.md

                          -

                          总之,不就是看寄存器都是啥东西,然后对应实现read write嘛!概念上是不难的,好好研究,相信可以!

                          -

                          接下来的思路就是,看下那几个寄存器具体的description,理解下两个参考的代码,就可以慢慢开始写了。测试可能需要再花点心思因为毕竟那个还跑步起来。。。总之方向算是比较明确了(至少比前几天明确)加油捏。

                          -

                          10.15—寄存器了解

                          这东西甚至是一个bit一个bit控制的

                          -
                          #define GPIO_SWPORTA_DR		0x00 //A 组端口输出寄存器
                          写入该寄存器的值独立控制端口A中对应数据位的方向。0是input,1是output
                          #define GPIO_SWPORTA_DDR 0x04 //A 组端口方向控制寄存器
                          #define GPIO_SWPORTB_DR 0x0c //B 组端口输出寄存器
                          #define GPIO_SWPORTB_DDR 0x10 //B 组端口方向控制寄存器
                          #define GPIO_SWPORTC_DR 0x18 //C 组端口输出寄存器
                          #define GPIO_SWPORTC_DDR 0x1c //C 组端口方向控制寄存器
                          #define GPIO_SWPORTD_DR 0x24 //D 组端口输出寄存器
                          #define GPIO_SWPORTD_DDR 0x28 //D 组端口方向控制寄存器

                          1. 这个东西可以用来控制每个bit的开关中断
                          2. 默认关中断
                          3. Output时关中断
                          #define GPIO_INTEN 0x30 //A 组端口中断使能寄存器
                          mask为1表示屏蔽
                          #define GPIO_INTMASK 0x34 //A 组端口中断屏蔽寄存器
                          控制是电平敏感中断还是边缘敏感
                          #define GPIO_INTTYPE_LEVEL 0x38 //A 组端口中断等级寄存器
                          极性
                          #define GPIO_INT_POLARITY 0x3c //A 组端口中断极性寄存器
                          只读
                          #define GPIO_INTSTATUS 0x40 //A 组端口中断状态寄存器
                          是否需要消抖【是不是很熟悉hhh】
                          #define GPIO_PORTA_DEBOUNCE 0x48 //A 组端口防反跳配置寄存器
                          清除A/所有中断
                          #define GPIO_PORTA_EOI 0x4c //A 组端口中断清除寄存器

                          只读,表示A端口连接的数据信号(也即input信号)
                          #define GPIO_EXT_PORTA 0x50 //A 组端口输入寄存器
                          #define GPIO_EXT_PORTB 0x54 //B 组端口输入寄存器
                          #define GPIO_EXT_PORTC 0x58 //C 组端口输入寄存器
                          #define GPIO_EXT_PORTD 0x5c //D 组端口输入寄存器

                          #define DWAPB_DRIVER_NAME "gpio-dwapb"
                          #define DWAPB_MAX_PORTS 4

                          // 步长。可以看到如它所言,确实GPIO_EXT_PORT每个差4,GPIO_SWPORTA_DR和GPIO_SWPORTA_DDR每个差12
                          #define GPIO_EXT_PORT_STRIDE 0x04 /* register stride 32 bits */
                          #define GPIO_SWPORT_DR_STRIDE 0x0c /* register stride 3*32 bits */
                          #define GPIO_SWPORT_DDR_STRIDE 0x0c /* register stride 3*32 bits */

                          #define GPIO_REG_OFFSET_V2 1

                          #define GPIO_INTMASK_V2 0x44
                          #define GPIO_INTTYPE_LEVEL_V2 0x34
                          #define GPIO_INT_POLARITY_V2 0x38
                          #define GPIO_INTSTATUS_V2 0x3c
                          #define GPIO_PORTA_EOI_V2 0x40

                          #define DWAPB_NR_CLOCKS 2

                          #define DWAPB_GPIO_BASE 0x03020000
                          - -

                          难道说,我有一个猜想,就是这个实际表示是说GPIO Port A/C的第16/17、9/10位吗。。。。

                          -

                          image-20231027203332238

                          -

                          所以现在很需要这个pin和port的转换。。。

                          -

                          一个是内核传过来的pin是什么数,还有它上面那个是不是端口和位的意思,这两点需要弄清楚

                          -

                          总之先向来是确认下我的想法:那个图的灰色块和引脚号是同一个东西,GPIOA17这样的字符表示GPIO的A端口寄存器的第16个bit。

                          -

                          如果是这样的话,那么思路就很自然了。我们可以通过图中引脚和GPIO这种的分布,从而得知当前是要写入什么端口什么寄存器,这样就能编写代码了。Good。

                          -

                          10.17—整亮led

                          也许可以搜一下schematic该怎么看,说不定就能知道哪位是引脚号了。

                          -

                          现在先假设GPIO C 24表示GPIO C端口的第24个bit,也即 (1<<23)吧。所以我们在读取和写入的时候,都是先读出原来的值,然后再把对应位设为1or0。但是为啥qemu是一字节一字节写。。。算了相信自己,总之先试试

                          -
                          -

                          然后就去研究了下linux中的gpio……发现了其内部映射。

                          +

                          特性:ACID

                          image

                          +

                          调度与可串行性

                          概念

                          image

                          +

                          image

                          +

                          image

                          +

                          image

                          +

                          图中最右错误,是因为这样的话B=B-20这个操作事实上就会被覆盖了。这两个事务不关心读的是不是B origin,只需要+10和-20操作都做了就行。

                          +

                          事务模型

                          image

                          +

                          冲突与可串行性

                          +

                          强烈推荐:冲突可串行化、事务优先图。PPT讲的不知道啥玩意

                          -
                          ~# cat /sys/class/gpio/gpiochip
                          gpiochip352/ gpiochip384/ gpiochip416/ gpiochip448/ gpiochip480/
                          - -

                          384是GPIOD,416是C,448是B,480是A,每个间距32。而LED对应的是GPIO C 24,所以号是416+24 = 440。

                          -

                          416+24 = 440,也就是说,GPIOC24是port C的第24号,也即第25位。

                          -

                          然后慢慢整理问题……

                          +

                          也就是说,如果一个schedule经过一系列对任意两个不冲突操作的调换动作后,可以变成一个完全串行的schedule,那么我们就称其为冲突可串行化。

                          -

                          整理一下目前的问题。

                          -

                          首先,我是不是想错了???

                          -

                          按照刚刚在Linux下看到的结果:

                          -
                          ~$ cat /sys/class/gpio/gpiochip
                          gpiochip352/ gpiochip384/ gpiochip416/ gpiochip448/ gpiochip480/
                          - -

                          五个gpio,bank name分别为porte、d、c、b、a。那么问题来了,这个port跟我们在dw_apb手册里看到的gpio的abcd四个port是一个概念吗??

                          -

                          已知,dwapb gpio有ABCD四个Port,因而有四组寄存器。有一点值得注意,到底是一个字节控制一个设备,还是一个bit?

                          -

                          那么我感觉的是,板上有5个gpio,每个gpio都各自有4个port和4K地址空间,每个port都有这样一堆寄存器:

                          -

                          image-20231027203540506

                          -

                          而GPIOC24的意思就是,它是第四个gpio?port夺少?说实话我真的很迷惑。。。

                          -

                          而且我那个写入为什么一点效果没有。。。好痛苦我的天

                          -

                          好,刚刚终于成功把led整亮了。所以是这样的,它那个GPIOABCD并不是一个GPIO的Port ABCD的意思,应该就是纯纯有多个GPIO芯片。然后GPIOC24的意思就是第三块GPIO的A组寄存器第32位,为什么是A不是别的我也不大懂,反正我看了Linux的驱动代码,好像一般也是只用A组端口。

                          -

                          10.18/19—中断

                          很好,目前已经基本完成了数据的读写这部分,接下来要做的就是中断了,中断还是先看看数据手册在做。中断的话,决定先研究下wiringX的那个example,然后来看看中断怎么进行有效性验证。

                          -

                          很好,把测试程序搬了过来,还有学习了下qemu的架构也搬过来了,现在剩下的任务就是看寄存器了

                          -

                          目前是这样,dwapb_pin_attach_irqdwapb_pin_detach_irq负责注册回调函数,dwapb_pin_irq_enable负责开关中断。然后,我们通过rt_hw_interrupt_install(DWAPB_GPIOE_IRQNUM, rt_hw_gpio_isr, &gpio_idx, "gpio");注册中断函数,这样一来每次DWAPB_GPIOE_IRQNUM中断时就会触发该函数,从而执行rt_hw_gpio_isr中遍历所有hdr并执行一遍的流程了。

                          -

                          整理一下它目前需要有哪些寄存器。首先是rt_hw_gpio_isr,需要有一个pending功能;然后是dwapb_pin_attach_irq,需要能设置是电平/边缘触发以及触发的极性;最后是dwapb_pin_irq_enable,需要有个能开关中断的寄存器。

                          -

                          目前是这样,理论上,我通过rt_pin_irq_enable向INTEN写入数据,这b对应的GPIO引脚就会产生一个中断,然后将中断传递给CPU,CPU通过其中断号从而调用中断处理函数,然后就吊我们在it_install中安装的那个回调。然而不知怎的没调。我觉得原因可能两个,一个寄存器看错了,另一个安装没装好。明天对照Linux源码看下吧。

                          -

                          应该不是,还是得靠外界输入才能触发中断。明天问问老师怎么做。

                          -

                          新进展,发现C9C10互连,然后C10间隔输出就行23333这样就可以触发中断,perfect。

                          -

                          不过我目前的问题似乎是,触发了中断之后,不知道为什么没有进入pending处理,导致中断一直未处理从而寄。

                          -

                          牛逼,成了!!!明天改下码风,多写几个测试,应该就结了。好像目前是both有点问题。明天把这个修下。

                          -

                          image-20231027203706048

                          -

                          BOTH_EDGE有点问题,好像那啥玩意没效果,依然保持着上次设置的lowedge。现在去学习下linux的both edge怎么写的。

                          -

                          很好,bug已除。

                          -

                          代码框架

                          GPIO实际上就是一个单纯的在板子和外设之间搬运字节的东西,所以本质上只需做好寄存器配置,以及引脚映射即可。接下来将从几个方面拆解我的代码。

                          -

                          整体架构

                          在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…

                          +

                          image

                          +

                          image

                          +

                          image-20231119184216295

                          +

                          image-20231119184114564

                          +

                          image

                          +

                          如果一个 schedule 和某个串行 schedule 冲突等价,则称该 schedule 是冲突可串行化(conflict serializable)的。

                          +

                          image

                          +

                          image

                          +

                          真不懂

                          +

                          image

                          +

                          冲突可串行判别算法

                          image

                          +

                          一种就是上面那个图片例子,一直交换直到得到串行调度;

                          +

                          另一种就是下面要介绍的这个画图了。

                          +

                          说实话真没懂这啥玩意冲突可串行

                          +

                          image

                          +

                          也没搞懂这灰线又tm哪来的。。。

                          +

                          image

                          +

                          基于封锁的并发控制

                          概述

                          image

                          +

                          image

                          +

                          image

                          +

                          image

                          +

                          不满足

                          +

                          考虑因素

                          锁的类型

                          image

                          +

                          封锁协议

                          image

                          +
                          相容性矩阵

                          image

                          +
                          加解锁时机

                          image

                          +
                          封锁粒度

                          image

                          +

                          两段封锁协议

                          两段封锁协议是一种基于锁的并发控制方法。

                          +

                          image

                          +

                          image

                          +

                          满足

                          +

                          image

                          +

                          注意这个两端锁与前面那个只用锁的差距。两段锁把B加锁也移到前面了,所以可以保证冲突可串行性。

                          +

                          image

                          +

                          读nm,不读再见

                          +

                          可以看到,图中确实是满足两段锁协议要求的,加锁段里确实没有解锁段。消除死锁的方式应该就是按照一定顺序获取锁。

                          +

                          image

                          +

                          基于时间戳的并发控制方法

                          概述

                          image

                          +

                          image

                          +

                          简单的调度规则

                          规则

                          image

                          +

                          image

                          +

                          示例

                          image

                          +

                          真没看懂为什么要撤回T2和T3

                          +

                          image

                          +

                          image

                          +

                          冲突是否可串行

                          image

                          +

                          image

                          +

                          另一种调度规则

                          解决问题

                          image

                          +

                          image

                          +

                          image

                          +

                          也就是说U没有提交,所以T要更新的反而也丢了,变得更加过时()

                          +

                          规则

                          image

                          +

                          image

                          +

                          image

                          +

                          image

                          +

                          基于有效性确认的并发控制

                          image

                          +

                          image

                          +

                          image

                          +

                          image

                          +

                          image

                          +

                          image

                          +

                          image

                          +

                          第十二章 数据库事务恢复技术

                          故障类型与影响

                          image

                          +

                          恢复基本思路

                          image

                          +

                          事务故障

                          image

                          +

                          系统故障

                          image

                          +

                          image

                          +

                          image

                          +

                          介质故障

                          image

                          +

                          冗余阵列

                          +

                          image

                          +

                          日志

                          是什么

                          image

                          +

                          image

                          +

                          此时应该上层已经确保了事务的调度是冲突可串行化的,所以这里不用考虑会不会脏读问题。

                          +

                          image

                          +

                          image

                          +

                          Undo型日志

                          记录规则

                          这对应的应该是Steal

                          +

                          image

                          +

                          image

                          +

                          恢复步骤

                          image

                          +

                          检查点

                          image

                          +

                          image

                          +

                          image

                          +

                          Redo型日志

                          记录规则

                          这对应的应该是No Steal

                          +

                          image

                          +

                          image

                          +

                          恢复步骤

                          image

                          +

                          检查点

                          image

                          +

                          结合型日志

                          image

                          +

                          记录规则

                          image

                          +

                          image

                          +

                          恢复步骤

                          image

                          +

                          感觉得先undo再redo?

                          +

                          image

                          +

                          image

                          ]]> - - intern - 开源的第一个月 @@ -8791,7 +8794,7 @@ url访问填写http://localhost/webdemo4_war/*.do

                          稍作总结

                          总之,这一个月来我学到了许多,同时也对我以前未曾涉足的空白领域做了许多探索,包括对设备树、对嵌入式开发、对Linux设备驱动等的学习,总体来说还是十分甚至九分地开心。希望下个月能再接再厉。

                          ]]> - mylife + intern
                          @@ -8919,458 +8922,132 @@ url访问填写http://localhost/webdemo4_war/*.do。 -

                          进入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/
                          - -
                          -

                          注:测试得发现本RocksDB负载同最新版本RocksDB不兼容,故而建议使用上述命令对应版本,也即v6.15.5。

                          -
                          -

                          修改CMakeLists:

                          -
                          vim CMakeLists.txt
                          - -
                            -
                          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
                          - - - -

                          运行COS

                          克隆COS用户态代码:

                          -
                          git clone https://gitlab.eduxiji.net/202318123111334/cos_userspace.git
                          cd cos_userspace
                          - -

                          编译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
                          +

                          进入COS内核

                          完成上述步骤后,重启虚拟机:

                          +
                          sudo reboot
                          -

                          然后就可以运行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
                          +

                          在进入GRUB界面时选择Advanced Ubuntu,然后选择内核版本6.4.0+即可。

                          +

                          COS用户态

                          在完成COS内核编译,并进入COS内核之后,就完成了COS的基本环境搭建。接下来,将介绍如何搭建COS用户态环境,从而运行Shinjuku Scheduler和RocksDB实验。

                          +

                          首先,确保所处内核正确:

                          +
                          $ uname -r
                          6.4.0+
                          -

                          克隆ghOSt内核:

                          -
                          git clone https://github.com/google/ghost-kernel
                          cd ghost-kernel/
                          +

                          依赖安装

                          apt包

                          安装编译用户态需要的包:

                          +
                          sudo apt-get install cmake python2 python3 libtbb-dev libsnappy-dev zlib1g-dev libgflags-dev libbz2-dev liblz4-dev libzstd-dev
                          -

                          生成内核编译配置文件:

                          -
                          make localmodconfig
                          -

                          对生成的.config配置文件做如下修改:

                          + +

                          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/
                          + +
                          +

                          注:测试得发现本RocksDB负载同最新版本RocksDB不兼容,故而建议使用上述命令对应版本,也即v6.15.5。

                          +
                          +

                          修改CMakeLists:

                          +
                          vim CMakeLists.txt
                          +
                            -
                          1. CONFIG_SYSTEM_TRUSTED_KEYS="debian/canonical-certs.pem"修改为CONFIG_SYSTEM_TRUSTED_KEYS=""
                          2. -
                          3. 添加CONFIG_SCHED_CLASS_GHOST=y
                          4. +
                          5. 搜索WITH_TBB,将这一项改为ON:

                            +
                            option(WITH_TBB "build with Threading Building Blocks (TBB)" ON)
                          6. +
                          7. 搜索ROCKSDB_LITE 确保这一项为OFF

                            +
                            option(ROCKSDB_LITE "Build RocksDBLite version" OFF)
                          -

                          编译安装内核:

                          -
                          make -j12 && sudo make modules_install && sudo make install
                          +

                          保存退出后进行编译安装:

                          +
                          mkdir build && cd build && cmake .. && make -j12 && sudo make install
                          -

                          修改grub

                          详见COS环境搭建的对应部分。在此不做赘述。

                          -

                          ghOSt用户态

                          依赖安装

                          bazel

                          -

                          详细安装可参照官方文档Installing Bazel on Ubuntu,在此仅给出其中第一种方法。

                          -
                          -

                          将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
                          +

                          运行COS

                          克隆COS用户态代码:

                          +
                          git clone https://gitlab.eduxiji.net/202318123111334/cos_userspace.git
                          cd cos_userspace
                          -

                          安装和更新Bazel

                          -
                          sudo apt update && sudo apt install bazel
                          # 将bazel升级到最新版本
                          sudo apt update && sudo apt full-upgrade
                          +

                          编译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
                          -

                          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
                          +

                          等待测试完成即可。

                          +

                          若要运行展示中所提到的测试,可详细见性能测试教程

                          +

                          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
                          -

                          运行ghOSt

                          克隆ghOSt用户态:

                          -
                          git clone https://gitlab.eduxiji.net/202318123111334/ghost_userspace.git
                          cd ghost-userspace
                          +

                          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
                          -]]> -
                          - - 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是一个指向该进程页表的指针。

                          -
                          -

                          这应该相当于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
                          -

                          其中start0:

                          -
                          __attribute__ ((aligned (16))) char stack0[4096 * NCPU];
                          -
                          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到内核态。

                          -
                          -

                          This instruction( mret ) is most often used to return from a previous call from supervisor mode to machine mode.

                          -
                          -

                          所以,我们实际上可以把最后两步连起来,用mret一个指令就完成。也即,mret指令既完成了从machine mode到内核态的切换,又完成了从start.c到main.c的跳转。

                          -

                          这其实很容易,只需在栈中将调用者(此时应该是entry.S)的地址替换为main.c的地址,并且将调用者的mode改为内核态,这样就ok了。

                          -
                          -

                          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一样关闭虚拟地址映射,还得把什么中断和异常委托给内核态??

                          -

                          【我猜测是因为现在页表还没初始化好所以当然得关闭虚拟地址映射();后者大概是开中断的意思?】

                          -
                          -

                          代码如下:

                          -
                          // 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");
                          }
                          +

                          rust-nightly

                          curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
                          rustup toolchain install nightly
                          rustup default nightly
                          # 查看rust版本
                          rustc --version
                          -
                          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内置指令,起内存屏障作用。在竞赛中深刻地了解过了内存屏障,在这里再次跟老熟人再会感觉还是很有意思的。

                          -
                          -
                          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);
                          }
                          -
                          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
                          +

                          内核编译

                          安装内核编译所需包:

                          +
                          sudo apt-get update && sudo apt-get install build-essential gcc g++ make libncurses5-dev libssl-dev bison flex bc libelf-dev
                          -
                          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
                            +

                            克隆COS内核:

                            +
                            git clone https://gitlab.eduxiji.net/202318123111334/ext-kernel.git
                            cd ext-kernel/
                            -

                            后者的话,从用户态切到内核态使用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");
                            +

                            生成内核编译配置文件:

                            +
                            make localmodconfig
                            -

                            因而从内核态切换到用户态应该也是需要类似这段对mstatus寄存器的修改的,并且其对应修改的是sstatus寄存器。

                            -

                            但是,我只在普通的用户态-trap入内核态-用户态这个过程的usertrapret中看到对sstatus寄存器的写入,并没有在init的时候对这个玩意进行写入。

                            -

                            所以,最后,我初步猜测,是会在scheduler()中的上下文切换中修改sstatus寄存器的内容为user mode,从而实现由内核态向用户态进程(initcode)的切换。不过这也仅仅是【猜想】,因为我并没有在switch的汇编代码中看到对sstatus的修改。真是令人麻木。。。

                            -
                          4. +

                            对生成的.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为开启状态
                            -

                            步骤十分直接且有理由:

                            -

                            加载操作系统——为了能执行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.

                          -
                          -

                          感想

                          一开始为了把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;
                          }
                          }
                          +

                          编译安装内核:

                          +
                          make -j12 && sudo make modules_install && sudo make install
                          -
                          // 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;
                          }
                          }
                          +

                          修改grub

                          详见COS环境搭建的对应部分。在此不做赘述。

                          +

                          EXT用户态

                          pwd # 确保在ext-kernel/项目根目录下
                          cd ext-kernel/tools
                          -

                          下面是按照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
                          };
                          +

                          随后,我们需要替换原有EXT使用示例为我们搭建的EXT用户态框架。

                          +
                          rm -rf sched_ext/
                          git clone https://gitlab.eduxiji.net/202318123111334/proj134-cfs-based-userspace-scheduler.git sched_ext
                          -
                          编写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;
                          }
                          }
                          +

                          然后就可以运行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
                          -
                          修改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;
                          }
                          }
                          +

                          克隆ghOSt内核:

                          +
                          git clone https://github.com/google/ghost-kernel
                          cd ghost-kernel/
                          -
                          在sysproc.c中添加系统调用
                          uint64
                          sys_trace(void)
                          {
                          int mask;
                          if(argint(0,&mask)<0)
                          return -1;
                          trace(mask);
                          return 0;
                          }
                          +

                          生成内核编译配置文件:

                          +
                          make localmodconfig
                          -
                          修改fork

                          继承父进程的mask

                          -
                          np->mask = p->mask;
                          +

                          对生成的.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
                          -
                          在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”.

                          + +

                          修改grub

                          详见COS环境搭建的对应部分。在此不做赘述。

                          +

                          ghOSt用户态

                          依赖安装

                          bazel

                          +

                          详细安装可参照官方文档Installing Bazel on Ubuntu,在此仅给出其中第一种方法。

                          -
                          // kernel/sysinfo.h
                          struct sysinfo {
                          uint64 freemem; // amount of free memory (bytes)
                          uint64 nproc; // number of process
                          };
                          +

                          将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
                          -

                          感想

                          代码

                          系统调用要做的事情同上。

                          -

                          有一个我在hit实验没想到,在这里依然没有想到的点是,参数的指针来自用户空间,所以不能直接对其指向的空间进行写入,需要借助copyout函数。

                          -

                          还有一件事,就是不知道该怎么统计free mem的数量,后来在hints提示下才知道要去kalloc.c中找。【之前只找过了vm.c】这里其实是很后悔提前看了提示的。我应该先去看一下上面关于kernel各个文件用途的笔记,再去继续自己找的,不能太过依赖提示。

                          -

                          还有一点做的不好的地方是,标答是选择了将两个计数函数放在各自的文件中,我是选择直接将成员变量在头文件中extern 公开出来,比如说在proc.h中这么写:

                          -
                          extern struct proc proc[NPROC];
                          +

                          安装和更新Bazel

                          +
                          sudo apt update && sudo apt install bazel
                          # 将bazel升级到最新版本
                          sudo apt update && sudo apt full-upgrade
                          -

                          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;
                          }
                          -
                          sysproc.c中
                          uint64
                          sys_sysinfo(void){
                          uint64 addr;
                          if(argaddr(0, &addr) < 0)
                          return -1;
                          return sysinfo((struct sysinfo*)addr);
                          }
                          -
                          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;
                          }
                          +

                          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
                          -
                          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.

                          -
                          -

                          这个实现起来要说简单也简单,麻烦也麻烦。这里就先摆了【实际上尝试了半小时发现太烦了看别人写的也不大满意就放弃了】

                          -
                          sysinfo plus
                          -

                          Compute the load average and export it through sysinfo

                          -
                          -

                          说实话没太看懂,不就加个 running process/ncpu就行了吗?

                          + +

                          运行ghOSt

                          克隆ghOSt用户态:

                          +
                          git clone https://gitlab.eduxiji.net/202318123111334/ghost_userspace.git
                          cd ghost-userspace
                          + ]]> @@ -9412,404 +9089,657 @@ url访问填写http://localhost/webdemo4_war/*.do。 ]]> - 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

                          + 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");
                          }
                          + +

                          这是一个利用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);
                          }
                          + +

                          也即父进程创建出子进程来执行command,并且父进程等待子进程执行完再继续等待输入。

                          +

                          可以看到,fork和exec的使用是非常紧密的,联合使用也是非常顺理成章的。那么,如果干从fork的exec的对于内存管理的原理来讲,就会不免产生一点问题。

                          -

                          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行左右。

                          +

                          问题描述:

                          +

                          fork的内存原理,实质上是开辟一片新的与父进程等大的内存空间,然后把父进程的数据都copy一份进这个新内存空间。exec的原理是用一片可以容纳得下文件指令及其所需空间的内存空间去替代调用进程原有的那片内存空间。

                          +

                          可以看到,如果fork和exec接连使用,理论上其实是会产生一点浪费的,fork创建子进程复制完了一片内存空间,这片新复制的内存空间又马上被扔掉了,取而代之的用的是exec的内存空间。

                          -

                          虚拟地址有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页。

                          -

                          三级页表结构相比于单级页表结构,可以节省更多内存空间

                          +

                          为了解决这个问题,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.

                          -

                          考虑到这样一个进程:

                          -

                          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的对比

                          +

                          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).

                          -

                          每个进程都有一个用户级别的页表。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.
                          // ...
                          +

                          句柄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);
                          }
                          }
                          + +

                          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);
                          }
                          + +

                          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
                          -

                          由图可知,一直从0x0到0x86400000,都是采取的直接映射的方式,虚拟地址=物理地址,这段是内核使用的空间。在0x0-0x800000000阶段,物理地址代表着各种IO设备的存储器。

                          -

                          但是注意,在0x86400000(PHYSTOP)以上的地址都不是直接映射,这些非直接映射的层级包含两类:

                          -
                            -
                          1. trampoline

                            -
                            -

                            It is mapped at the top of the virtual address space; user page tables have this same mapping.

                            -
                            -

                            它有一点很特殊的是,它实际对应的物理内存是0x80000000开始的一段。也就是说,0x80000000开始的这段内存,既被直接映射了,也被trampoline通过虚拟地址映射了。它被映射了两次。

                            -
                          2. -
                          3. 内核栈

                            -

                            Each process has its own kernel stack, which is mapped high so that below it xv6 can leave an unmapped guard page. The guard page’s PTE is invalid (i.e., PTE_V is not set), so that if the kernel overflflows a kernel stack, it will likely cause an exception and the kernel will panic.

                            -

                            guard page可以用来防止内核栈溢出。

                            +

                            这个指令的意思是,先把stderr的结果重定向到stdout,再把stdout的结果重定向到tmp1中。

                            +

                            关于2>&1的解释,可以看这个 shell中的”2>&1”是什么意思?

                            -
                          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;
                          }
                          +

                          这个的实现就要用到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]);
                          }
                          -

                          其中,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");
                          }
                          +

                          完成了父进程-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;
                          -

                          实现主要逻辑的是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;
                          }
                          +

                          这实际上是二叉树的左右中递归过程。

                          +
                          +

                          附:对于管道命令的解读

                          +
                          cat a.txt | echo
                          -

                          通过虚拟地址获取表项主要是通过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)];
                          }
                          +

                          我的本意是觉得,这意思就是把cat a.txt的输出连到echo的输入,这个命令结果跟cat a.txt是没什么差的。但具体执行出来发现最后的结果却是跟:

                          +
                          echo
                          -
                          装上页表

                          使用的是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();
                          }
                          +

                          这个指令的效果是一样的,也就是cat a.txt的output,即echo的input完全被丢弃了。

                          +

                          我想这是因为,echo这个命令的执行过程并没有用到stdin,仅仅用到了参数,也就是说管道read端的接入对它并没有什么影响。

                          +

                          这也是为啥

                          +
                          sleep 10 | echo hi
                          -

                          其中sfence_vma()的用途是强制更新TLB的旧页表,类似于Java volatile的作用。

                          -
                          疑问

                          附上书里的详细解释:

                          -

                          image-20230109222917346

                          -

                          TLB与页表类似于cache与主存的关系。TLB保存了页表的一部分。

                          -
                          我的错误想法

                          我怎么感觉怪怪的啊?因为TLB既然是高速缓存,那么读写页表也应该优先从TLB读写【注:应该就是从这里开始错的hhh写应该是直接写入页表】。所以说,会陈旧的应该是主存中的页表,而不是TLB中的页表。但是,书里是说,改完页表必须通知TLB更改。也就是说,读写页表不是从TLB读写的,那该是从哪里?是TLB以外的free memory吗?

                          -

                          不过,要是从多CPU的角度思考,说不定他这个意思是某个CPU的TLB变了,需要通知其他所有CPU的TLB也变。虽然不同CPU当前执行的进程是不一样的,使用的页表项不一样,切换进程的时候也会把用户地址空间的页表项flush掉。但是内核地址空间的页表项一般是不会随着进程切换而flush掉的。所以内核页表修改就需要手动多CPU同步。

                          -

                          我认为多CPU角度考虑更加合理,因为它最后说了,xv6会在内核页表init后flush,以及在从内核态切换回用户态的时候flush。这两个(好像)都影响内核页表比较多,所以就需要手动flush一下。

                          -
                          解答

                          之后学了缺页异常后,可以发现这里其实是没问题的。

                          -

                          计算机体系结构 – 虚拟内存

                          -

                          v2-e15454bf032baa4dc088b6e41ed4f4a4_1440w

                          -

                          页表的管理(创建、更新、删除等)是由操作系统负责的。地址转换时,页表检索是由硬件内存管理单元(Memory Management Unit, MMU)负责的。MMU通常由两部分构成:表查找单元(Table Walk Unit, TWU)和转换旁路缓冲(Translation Lookaside Buffer, TLB)[2]。TWU负责链式的访问PDE、PTE,完成上述的查表过程。

                          -

                          应用多级页表之后,想要完成一次地址转换,需要访问多级目录和页表,这么多次的内存访问会严重降低性能。

                          -

                          为了优化地址转换速度,人们在MMU中增加了一块高速cache,专门用来缓存虚拟地址到物理地址的映射,这块cache就是TLB[7][8]。MMU在做地址转换的时候,会先检索TLB,如果命中则直接返回对应的物理地址,如果不命中则会调用TWU查找页表。

                          -

                          TLB中缓存的是虚拟地址到物理地址映射。然而,多级页表的查找是一个链式的过程,对于在虚拟地址空间中连续的两个页,它们的各级目录项可能都是一样的,只有最后一级页号不一样。查找完第一个虚拟页之后,我们可以将相同的前级目录项都缓存起来。查找第二个虚拟页时,可以直接使用缓存好的前几级目录项,节省查找时间。这种缓存叫做Page Structure Cache[9]

                          -

                          而当TLB和MMU中都没有该物理页,就会发生缺页异常。但是操作系统仅会对页表更新,而不会被TLB更新。故而,TBL中数据可能陈旧,需要手动flush。

                          -

                          Physical memory allocation

                          在内核运行的时候,需要申请很多空间用来存放各种数据。

                          -
                          -

                          The kernel must allocate and free physical memory at run-time for page tables, user memory, kernel stacks, and pipe buffers.

                          +

                          这个命令最后的结果是,秒速出hi,然后等待10s后结束,了。由于echo的输出与stdin没有关系,所以,echo不会阻塞读入stdin,等待管道关闭,而是会即刻输出hi。

                          -

                          用的是这段空闲内存:

                          -

                          image-20230109225700837

                          -
                          -

                          It keeps track of which pages are free by threading a linked list through the pages themselves.

                          +

                          管道实际上就相当于:

                          +
                          echo hello world | wc
                          echo hello world > /tmp/xyz; wc < /tmp/xyz
                          + +

                          在这种情况下,管道相比临时文件至少有四个优势

                          +
                            +
                          • 首先,不用删文件
                          • +
                          • 其次,管道可以任意传递长的数据流
                          • +
                          • 第三,管道允许一定程度上的并行
                          • +
                          • 第四,如果实现进程间通讯,管道的块读写比文件的非块语义更有效率。
                          • +
                          +

                          File system

                          inode:代表文件本体,包括文件类型、文件长度、文件内容在磁盘位置、文件的链接数

                          +

                          link:指向文件的链接,一个文件可以有多个link,link内包含文件名和对inode的引用

                          +

                          当链接数=0,且句柄数=0,文件的磁盘空间和inode索引就会被释放

                          +

                          Lab Xv6 and Unix utilities

                          配置实验环境

                          +

                          参考文章:

                          +

                          xv6环境搭建

                          +

                          【MIT6.S081/6.828】手把手教你搭建开发环境

                          -

                          kalloc.c中就是这么实现的。

                          -

                          Code: Physical memory allocator

                          内核运行时申请释放空闲物理空间是通过kernel/kalloc.c完成的。它为内核栈、用户进程、页表和管道buffer服务。

                          +

                          下载工具链

                          $ sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu 
                          + +

                          测试安装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
                          ...
                          +
                          -

                          kalloc.c用来在运行时申请分配新的一页,上面的vm.c正是用了kalloc申请一页,要么作为页表,要么作为存储数据的第三级页表指向的物理内存。

                          +

                          注,这里出现了一个问题,qemu-system-riscv64 --version打出来发现qemu-system-riscv64 command not found。似乎是我的ubuntu16.04版本太低了【悲】去看了下网上,可以按照这个来做:

                          +

                          rCore qemu risc-v 实验环境配置

                          -

                          最后应该会在空闲内存内形成这样的结构:

                          -

                          内存分成一页一页的,每页内存中的前几个字节存储着其对应队列中下一块内存的物理地址。不一定是从小地址到大地址顺序连接。

                          -
                          -

                          It store each free page’s run structure in the free page itself, since there’s nothing else stored there.

                          +

                          下载编译xv6源码

                          随后,进入一个你喜欢的文件夹clone xv6的实验源码,输入

                          +
                          $ git clone git://g.csail.mit.edu/xv6-labs-2020
                          $ cd xv6-labs-2020
                          $ git checkout util
                          + +

                          然后进行编译

                          +
                          $ make
                          $ make qemu
                          + +

                          如果此处发生错误: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
                          + +

                          再执行一次

                          +
                          $ make
                          $ make qemu
                          + +

                          就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.

                          -
                          // 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;
                          }
                          +
                          体会

                          思路很简单,我之所以写了那么久是因为走了好大的弯路……

                          +

                          题目要求输出格式为”: received ping”,我的思路固化为:先把pid化成数字,再用字符串拼接串成整个。为了实现我的思路,我就需要额外再写两个工具函数,一个是itoa,一个是strcat。而又由于malloc函数暂待实现,itoa和strcat的实现就仍然不够优雅。折腾了半天终于OK了,结果看到别人是怎么做到这个输出格式的呢?↓

                          +
                          fprintf(1,"%d: received ping\n",getpid());
                          -

                          Process address space

                          当用户进程叫xv6分配内存时,xv6会用kalloc去取,然后登记在页表上。

                          -
                          -

                          The stack is a single page, and is shown with the initial contents as created by exec. Strings containing the command-line arguments, as well as an array of pointers to them, are at the very top of the stack. Just under that are values that allow a program to start at main as if the function main(argc, argv) had just been called.

                          +

                          这下是真的尴尬了23333

                          +

                          但总而言之,自己写了那俩不够优雅的函数还算是有点用【大概】。以下是我的代码

                          +

                          编写primes

                          +

                          参考:

                          +

                          MIT操作系统实验lab1(案例:primes(质数筛选)附代码、详解)

                          +

                          XV6实验-Lab0 Utilities

                          -

                          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).

                          +
                          +

                          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.

                          -
                          // 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;
                          }
                          - -
                          // Allocate PTEs and physical memory to grow process from oldsz to
                          // newsz, which need not be page aligned.不需要页对齐 Returns new size or 0 on error.
                          uint64
                          uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz, int xperm)
                          {
                          char *mem;
                          uint64 a;

                          if(newsz < oldsz)
                          return oldsz;

                          // oldsz向上取整
                          oldsz = PGROUNDUP(oldsz);
                          // 每页alloc
                          for(a = oldsz; a < newsz; a += PGSIZE){
                          mem = kalloc();
                          if(mem == 0){
                          // 说明失败,恢复到原状
                          // 这里不用像下面一样kfree是因为这里压根没有alloc成功
                          uvmdealloc(pagetable, a, oldsz);
                          return 0;
                          }
                          // 除去junk data
                          memset(mem, 0, PGSIZE);
                          // 放入页表
                          if(mappages(pagetable, a, PGSIZE, (uint64)mem, PTE_R|PTE_U|xperm) != 0){
                          // 不成功
                          // dealloc原理是顺着页表一个个free的。由于mem此处没有成功放入页表,所以就得单独free掉
                          kfree(mem);
                          uvmdealloc(pagetable, a, oldsz);
                          return 0;
                          }
                          }
                          return newsz;
                          }
                          - -

                          Code: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是创建地址空间的用户部分的系统调用。它使用一个存储在文件系统中的文件初始化地址空间的用户部分。

                          +

                          其实就是用生产者消费者模式来写素数计算的并发版本,这个我熟

                          +

                          ……以上是第一印象。然后我看着超链接文章里的素数筛的图片,以及指导书给的提示:

                          +
                          +

                          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.
                          • +
                          -
                          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;
                          }
                          +

                          义无反顾地……使用了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);
                          }
                          -

                          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.

                          +

                          编写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.

                          -

                          不过遗憾的是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

                          +
                          初始版

                          直接照着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.

                          -

                          感想

                          乌龙

                          这里好像是因为实验改版了,我下的是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;
                          };
                          +
                          体会

                          思路还是很直观的,就是从stdin一行一行读入数据,然后把这数据处理成参数,最后调用exec就行。就是中间有很多小细节值得注意。

                          +

                          有一点比较坑的是,main方法的那个argc的计算方法是这样的,不是直接用数组的长度:

                          +
                          for(argc = 0; argv[argc]; argc++) 
                          -

                          用户的ugetpid只找到了一个截图:

                          -

                          v2-0c2603da4c8102e46ae390a0d0b1191d_1440w

                          -

                          恕我愚钝实在不知道该把这段代码放在哪orz于是接下来写的东西就没有自测。

                          -
                          panic:freewalk leaf

                          一开始写好代码准备启动xv6的时候爆出了这么一个panic,搜了一下得到如下解答:

                          +

                          可以看到,合格的argv的形式应该是:参1 参2 参3 “\0”,最后一个元素要以”\0”标志结束。

                          +

                          这个应该是编写者约定俗成的。在user/sh.c的parseexec,大概445行左右:

                          +

                          image-20230106172133338

                          +

                          shell处理命令时是会默认把最后一个清零的。

                          -

                          来源:MIT-6.S081-2020实验(xv6-riscv64)十:mmap

                          -

                          这时运行会发现freewalk函数panic:freewalk: leaf,这是因为freewalk希望所有虚拟地址已经被解绑并释放对应的物理空间了,该函数只负责释放页表。

                          -
                          -

                          让我得知freewalk在vm.c下面【吐槽,我一开始还以为是自由自在地走(,看到这个才反应过来是free walk,跟页表有关的】。结合freewalk的代码

                          -

                          image-20230110225359361

                          -

                          可以知道,造成这个panic的原因是需要手动释放页表项。而在这里

                          -
                          // in proc.c  freeproc()
                          if(p->usyscall)
                          kfree((void*)p->usyscall);
                          p->usyscall = 0;
                          - -

                          仅仅是释放掉了对应的物理页,页表项并没有被释放

                          -

                          对比了一下别人写的,才发现原来这里也需要修改:

                          -
                          // Free a process's page table, and free the
                          // physical memory it refers to.
                          void
                          proc_freepagetable(pagetable_t pagetable, uint64 sz)
                          {
                          uvmunmap(pagetable, TRAMPOLINE, 1, 0);
                          uvmunmap(pagetable, TRAPFRAME, 1, 0);
                          //添加此句
                          uvmunmap(pagetable, USYSCALL, 1, 0);
                          uvmfree(pagetable, sz);
                          }
                          - -

                          这样一来,问题就解决了。

                          -
                          总结

                          因而,可以看到,如果进程想使用页的话,需要经历以下四步:

                          -
                            -
                          1. 通过kalloc获取物理页地址(可以通过该地址对页进行读写),并且记录在进程proc结构中(否则之后就获取不了了)
                          2. -
                          3. 建立mappages映射
                          4. -
                          5. 释放物理页
                          6. -
                          7. 释放PTE映射
                          8. -
                          -

                          可见12和34都是分别一一对应的。

                          -

                          代码

                          // Look in the process table for an UNUSED proc.
                          // If found, initialize state required to run in the kernel,
                          // and return with p->lock held.
                          // If there are no free procs, or a memory allocation fails, return 0.
                          static struct proc*
                          allocproc(void)
                          {
                          struct proc *p;

                          //有线程池那味了
                          for(p = proc; p < &proc[NPROC]; p++) {
                          acquire(&p->lock);
                          if(p->state == UNUSED) {
                          goto found;
                          } else {
                          release(&p->lock);
                          }
                          }
                          return 0;

                          found:
                          p->pid = allocpid();

                          // Allocate a trapframe page.
                          if((p->trapframe = (struct trapframe *)kalloc()) == 0){
                          release(&p->lock);
                          return 0;
                          }
                          // Allocate a usyscall page.
                          if((p->usyscall = (struct usyscall *)kalloc()) == 0){
                          release(&p->lock);
                          return 0;
                          }
                          //在USYSCALL写入usyscall结构体
                          p->usyscall->pid = p->pid;

                          // An empty user page table.
                          p->pagetable = proc_pagetable(p);
                          if(p->pagetable == 0){
                          freeproc(p);
                          release(&p->lock);
                          return 0;
                          }

                          // Set up new context to start executing at forkret,
                          // which returns to user space.
                          memset(&p->context, 0, sizeof(p->context));
                          p->context.ra = (uint64)forkret;
                          p->context.sp = p->kstack + PGSIZE;

                          return p;
                          }

                          // free a proc structure and the data hanging from it,
                          // including user pages.
                          // p->lock must be held.
                          static void
                          freeproc(struct proc *p)
                          {
                          if(p->trapframe)
                          kfree((void*)p->trapframe);
                          p->trapframe = 0;
                          if(p->pagetable)
                          proc_freepagetable(p->pagetable, p->sz);
                          p->pagetable = 0;
                          if(p->usyscall)
                          kfree((void*)p->usyscall);
                          p->usyscall = 0;
                          p->sz = 0;
                          p->pid = 0;
                          p->parent = 0;
                          p->name[0] = 0;
                          p->chan = 0;
                          p->killed = 0;
                          p->xstate = 0;
                          p->state = UNUSED;
                          }

                          // Create a user page table for a given process,
                          // with no user memory, but with trampoline pages.
                          pagetable_t
                          proc_pagetable(struct proc *p)
                          {
                          pagetable_t pagetable;

                          // An empty page table.
                          pagetable = uvmcreate();
                          if(pagetable == 0)
                          return 0;

                          // map the trampoline code (for system call return)
                          // at the highest user virtual address.
                          // only the supervisor uses it, on the way
                          // to/from user space, so not PTE_U.
                          if(mappages(pagetable, TRAMPOLINE, PGSIZE,
                          (uint64)trampoline, PTE_R | PTE_X) < 0){
                          uvmfree(pagetable, 0);
                          return 0;
                          }

                          // map the trapframe just below TRAMPOLINE, for trampoline.S.
                          if(mappages(pagetable, TRAPFRAME, PGSIZE,
                          (uint64)(p->trapframe), PTE_R | PTE_W) < 0){
                          uvmunmap(pagetable, TRAMPOLINE, 1, 0);
                          uvmfree(pagetable, 0);
                          return 0;
                          }

                          // 映射USYSCALL
                          if(mappages(pagetable, USYSCALL, PGSIZE,
                          (uint64)(p->usyscall), PTE_R|PTE_U) < 0){
                          uvmunmap(pagetable, TRAMPOLINE, 1, 0);
                          uvmunmap(pagetable, TRAPFRAME, 1, 0);
                          uvmfree(pagetable, 0);
                          return 0;
                          }
                          return pagetable;
                          }

                          // Free a process's page table, and free the
                          // physical memory it refers to.
                          void
                          proc_freepagetable(pagetable_t pagetable, uint64 sz)
                          {
                          uvmunmap(pagetable, TRAMPOLINE, 1, 0);
                          uvmunmap(pagetable, TRAPFRAME, 1, 0);
                          uvmunmap(pagetable, USYSCALL, 1, 0);
                          uvmfree(pagetable, sz);
                          }
                          - -

                          问答题

                          -

                          Which other xv6 system call(s) could be made faster using this shared page? Explain how.

                          +

                          确实,后面在学内存的时候,用户空间的构成如图所示:

                          +

                          image-20230109234930690

                          +

                          可以看到栈那边,参数列完了之后是会有一个用以terminate的空指针的

                          -

                          我觉得如果能在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.

                          +

                          附加题:改善shell

                          看起来又难又多所以我先摸了【润】等之后有时间再回来弄吧

                          +]]> + + + 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.
                          • +
                          -

                          感想

                          image-20230111000329475

                          -

                          很可惜,我在上面检索freewalk leaf到底是什么东西的时候,不小心看到了这题需要去参照freewalk这个提示【悲】其实我觉得这点还是需要绕点弯才能想到的,可能直接想到有点难【谁知道呢,世界线已经变动了】。

                          -

                          它这个打印页表其实最主要是考查如何遍历页表,这让人想起了walk这样的东西。但是walk是根据虚拟地址一级级找PTE的,中间很多地方会被跳过。有没有一个过程会在做事的时候遍历整个页表呢?答案是,这个过程就是释放页表的过程。释放页表才会一个个地看是否需要释放。释放页表的函数是freewalk,因而这道题参考freewalk的代码即可。

                          -

                          我觉得从“遍历页表”联想到“释放页表”这点是很巧的。不过也不会很突兀,毕竟学数据结构时就知道释放就需要遍历,逆向思维有点难但问题不大。

                          -

                          其他的就都挺简单的,不多赘述。

                          -

                          代码

                          记得在defs.h中添加声明

                          -
                          //在vm.c下
                          void
                          vmprint_helper(pagetable_t pagetable,int level)
                          {
                          // there are 2^9 = 512 PTEs in a page table.
                          for(int i = 0; i < 512; i++){
                          pte_t pte = pagetable[i];
                          if(pte & PTE_V){
                          for(int j=0;j<level;j++){
                          printf(" ..");
                          }
                          printf("%d: pte %p pa %p\n",i,(uint64)pte,(uint64)(PTE2PA(pte)));
                          if((pte & (PTE_R|PTE_W|PTE_X)) == 0){
                          // this PTE points to a lower-level page table.
                          uint64 child = PTE2PA(pte);
                          vmprint_helper((pagetable_t)child,level+1);
                          }
                          }
                          }
                          }

                          // 打印页表
                          void
                          vmprint(pagetable_t pagetable)
                          {
                          // typedef uint64 *pagetable_t;所以pagetable可以以%p形式打印
                          printf("page table %p\n",(uint64)pagetable);
                          vmprint_helper(pagetable,1);
                          }
                          - -

                          问答题

                          -

                          Explain the output of vmprint in terms of Fig 3-4 from the text.

                          -

                          What does page 0 contain?

                          -

                          What is in page 2? When running in user mode, could the process read/write the memory mapped by page 1?

                          -

                          What does the third to last page contain?

                          +

                          这章主要是讲了操作系统为了兼顾并发性、隔离性、交互性做出的基本架构。

                          +

                          Kernel organization

                          宏内核与微内核

                          操作系统一个很重要的设计问题就是,哪部分的代码需要run在内核态,哪部分的需要run在用户态。

                          +

                          如果将操作系统所有系统调用统统都在内核态run,这种设计方式就叫宏内核monolithic kernel

                          +

                          如果仅将系统调用中必要的部分在内核态run,其他部分都在用户态run,并且采取Client/Server这样的异步通信方式,这种设计方式就叫微内核microkernel

                          +

                          image-20230107232802540

                          +
                          +

                          由于客户/服务器(Client/Server)模式,具有非常多的优点,故在单机微内核操作系统中几乎无一例外地都采用客户/服务器模式,将操作系统中最基本的部分放入内核中,而把操作系统的绝大部分功能都放在微内核外面的一组服务器(进程)中实现。

                          -

                          从上面操作系统的启动来看,进程1应该是在main.c中的userinit()中创建的进程,也是shell的父进程。【确实,经实践可得shell的pid为2】

                          -

                          可以来看一下userint的代码:

                          -
                          void
                          userinit(void)
                          {
                          struct proc *p;

                          p = allocproc();
                          initproc = p;

                          // 申请一页,将initcode的指令和数据放进去
                          // allocate one user page and copy initcode's instructions
                          // and data into it.
                          /*
                          uvminit的注释:
                          // Load the user initcode into address 0 of pagetable,
                          // for the very first process.
                          // sz must be less than a page.
                          */
                          uvminit(p->pagetable, initcode, sizeof(initcode));
                          p->sz = PGSIZE;

                          //为内核态到用户态的转变做准备
                          // prepare for the very first "return" from kernel to user.
                          /*
                          Trap Frame是指中断、自陷、异常进入内核后,在堆栈上形成的一种数据结构
                          */
                          p->trapframe->epc = 0; // user program counter
                          p->trapframe->sp = PGSIZE; // user stack pointer

                          // 修改进程名
                          safestrcpy(p->name, "initcode", sizeof(p->name));
                          p->cwd = namei("/");

                          //这个也许是为了能被优先调度
                          p->state = RUNNABLE;

                          release(&p->lock);
                          }
                          - -

                          可见,page0是initcode的代码和数据,page1和page2用作了进程的栈,其中page1应该是guard page,page2是stack。

                          -

                          不过这里从exec的角度解释其实更通用

                          -
                          int
                          exec(char *path, char **argv)
                          {
                          //分配新页表
                          if((pagetable = proc_pagetable(p)) == 0)
                          goto bad;

                          //elfhd应该指的是可执行文件头
                          // Load program into memory.
                          for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
                          //...
                          //总之顺利读到了
                          uint64 sz1;
                          //读到了就给它分配新空间并且填入页表
                          if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz, flags2perm(ph.flags))) == 0)
                          goto bad;
                          sz = sz1;
                          }

                          //读完文件,开始造一个新的用户栈【fork之后用户栈是不会清空的】
                          sz = PGROUNDUP(sz);
                          uint64 sz1;
                          if((sz1 = uvmalloc(pagetable, sz, sz + 2*PGSIZE, PTE_W)) == 0)
                          goto bad;
                          sz = sz1;
                          // mark a PTE invalid for user access.造guard page
                          uvmclear(pagetable, sz-2*PGSIZE);
                          // sp为栈顶
                          sp = sz;
                          // 应该指的是栈尾
                          stackbase = sp - PGSIZE;
                          //...
                          }
                          - -

                          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.

                          +

                          在微内核中,内核接口由一些用于启动应用程序、发送消息、访问设备硬件等的低级功能组成。这种组织允许内核相对简单,因为大多数操作系统驻留在用户级服务器中。

                          +

                          像大多数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是一个指向该进程页表的指针。

                          +
                          +

                          这应该相当于pcb表。

                          +

                          Code: starting xv6 and the first process

                          看完一遍说实话还乱乱的。。。。我整理整理跟linux的对比学习一下吧。

                          +

                          xv6

                          加载操作系统

                          系统加电,启动BIOS初始化硬件 -> BIOS从引导扇区将加载程序读入内存 -> 加载程序将操作系统镜像读入内存RAM。

                          -

                          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.

                          +

                          这个过程由qemu模拟。

                          +

                          首先会通过mkfs造出操作系统镜像。然后由qemu将引导扇区,也即下面的filesys这图里的第0块:

                          +

                          image-20230121162324747

                          +

                          读入到主存中,然后开始执行引导扇区的程序,下同。

                          -

                          感想

                          实验内容:

                          -

                          实现void pgaccess(uint64 sva,int pgnum,int* bitmask);,一个系统调用。在这里面,我们要做的是,访问从svasva+pgnum*PGSIZE这一范围内的虚拟地址对应的PTE,然后查看PTE的标记项是否有PTE_A。有的话则在bitmask对应位标记为1.

                          -

                          应该注意的点:

                          -

                          1.需要进行内核态到用户态的参数传递 2.需要进行系统调用的必要步骤 3.PTE_A需要自己定义

                          -

                          以上是初见。做完了发现,确实就是那么简单,我主要时间花费在下的实验版本不对,折腾来折腾去了可能有一个小时,最后还是选择了直接把测试函数搬过来手工调用。已经换到正确的年份版本了【泪目】

                          -

                          有一点我忽视了,看了提示才知道:

                          +

                          boot loader目的是把xv6加载进内存到0x8000 0000,然后跳转到xv6初始化程序。

                          -

                          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).

                          +

                          The reason it places the kernel at 0x80000000 rather than 0x0 is because the address range 0x0:0x80000000 contains I/O devices.

                          -

                          也就是说每次检查到一个,就需要手动清除掉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);
                          }
                          +

                          操作系统初始化

                          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
                          -
                          // 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;
                          }
                          +

                          其中start0:

                          +
                          __attribute__ ((aligned (16))) char stack0[4096 * NCPU];
                          -

                          A kernel page table per process

                          -

                          The goal of this section and the next is to allow the kernel to directly dereference user pointers.

                          +
                          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到内核态。

                          +
                          +

                          This instruction( mret ) is most often used to return from a previous call from supervisor mode to machine mode.

                          +

                          所以,我们实际上可以把最后两步连起来,用mret一个指令就完成。也即,mret指令既完成了从machine mode到内核态的切换,又完成了从start.c到main.c的跳转。

                          +

                          这其实很容易,只需在栈中将调用者(此时应该是entry.S)的地址替换为main.c的地址,并且将调用者的mode改为内核态,这样就ok了。

                          -

                          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.

                          +

                          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一样关闭虚拟地址映射,还得把什么中断和异常委托给内核态??

                          +

                          【我猜测是因为现在页表还没初始化好所以当然得关闭虚拟地址映射();后者大概是开中断的意思?】

                          -

                          感想

                          这个其实平心而论不难,思路很简单。写着不难是不难,但想明白花费了我很多时间。

                          -

                          它这个要求我们修改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();
                          +

                          代码如下:

                          +
                          // 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");
                          }
                          -
                          // 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;
                          }
                          +
                          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();
                          }
                          -

                          这样会死得很惨,爆出如下panic:

                          -

                          image-20230114011100370

                          -

                          通过hints的调试贴士

                          -

                          A missing page table mapping will likely cause the kernel to encounter a page fault. It will print an error that includes sepc=0x00000000XXXXXXXX. You can find out where the fault occurred by searching for XXXXXXXX in kernel/kernel.asm.

                          +

                          注:关于里面的cpuid,我查了一下,指的是CPU的序列号,用来唯一标识cpu的。我想这个if架构的目的应该跟fork()==0差不多。也就是说,一开始的那个init仅有cpuid==0的CPU执行,其他的CPU就乖乖wait,只有CPU0执行初始化的程序。等到CPU0执行完所有init,才置标记位start=1,然后通过条件变量start控制抢占调度,轮流初始化自己。其中__sync_synchronize是GNU内置指令,起内存屏障作用。在竞赛中深刻地了解过了内存屏障,在这里再次跟老熟人再会感觉还是很有意思的。

                          -

                          我发现程序在这里绷掉了:

                          -
                          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();
                          }
                          - -

                          创建完进程后,就进入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;
                          // ...
                          - -

                          因而,c->proc是在创建进程的第一次调度后初始化的,也即,myproc只有在执行第一次scheduler之后才可以调用。而!!!

                          -

                          当执行调度前的userinit时:

                          -
                          void
                          userinit(void)
                          {
                          struct proc *p;

                          p = allocproc();
                          initproc = p;
                          - -

                          它进行了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();
                          }
                          - -

                          当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);
                          - -

                          但是这样还没有结束。因为我们除了得更换目前的页表,还得更换trapframe中的内核页表相关的东西:

                          -
                          struct trapframe {
                          /* 0 */ uint64 kernel_satp; // kernel page table
                          /* 8 */ uint64 kernel_sp; // top of process's kernel stack
                          }
                          - -

                          为啥还要更换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
                          - -

                          所以,为了以后系统调用能顺利自发进行,我们需要把栈帧也一起换掉。怎么换呢?我们是否还要在一些地方人工把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;
                          - -
                          // in vm.c
                          pagetable_t
                          perproc_kvminit()
                          {
                          pagetable_t pt = (pagetable_t) kalloc();
                          memset(pt, 0, PGSIZE);

                          // uart registers
                          pkvmmap(pt,UART0, UART0, PGSIZE, PTE_R | PTE_W);

                          // virtio mmio disk interface
                          pkvmmap(pt,VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);

                          // CLINT
                          pkvmmap(pt,CLINT, CLINT, 0x10000, PTE_R | PTE_W);

                          // PLIC
                          pkvmmap(pt,PLIC, PLIC, 0x400000, PTE_R | PTE_W);

                          // map kernel text executable and read-only.
                          pkvmmap(pt,KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);

                          // map kernel data and the physical RAM we'll make use of.
                          pkvmmap(pt,(uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);

                          // map the trampoline for trap entry/exit to
                          // the highest virtual address in the kernel.
                          pkvmmap(pt,TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
                          return pt;
                          }
                          - -
                          // in 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");
                          }
                          - -
                          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");
                          }
                          +
                          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);
                          }
                          -
                          修改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;
                          }
                          +
                          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
                          -
                          释放
                          // in kernel.proc.c freeproc()
                          if(p->kpgtbl)
                          proc_freekpgtbl(p->kpgtbl,p->kstack);
                          p->kpgtbl = 0;
                          +
                          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
                            -
                            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);
                            }
                            +

                            后者的话,从用户态切到内核态使用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");
                            -

                            Simplify copyin/copyinstr

                            -

                            参考:

                            -

                            6.S081学习记录-lab3

                            +

                            因而从内核态切换到用户态应该也是需要类似这段对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位模式也叫保护模式。

                          -

                          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.

                          +

                          至于怎么切的呢?要注意到一点,改变寻址方式也即改变cs和ip的地址计算方法,也即换一条硬件电路实现。计算机给我们提供了一个简单的方式操纵保护模式的转变,即修改cr0寄存器的内容。

                          +

                          在保护模式下,寻址方式发生了改变。此时cs不再代表基址,而是表示地址在gdb表global description table中的偏移下标。真正的基址放在表项中。cs被称为selector,从表中取得基址,再和ip加在一起得到地址。

                          -

                          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.

                          +

                          gdt表的内容由setup初始化

                          +

                          image-20230108013156116

                          -

                          感想

                          这题很直观的思路是,在每个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);
                          }
                          +

                          这样一来,就正确跳到了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.

                          +
                          +

                          感想

                          一开始为了把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;
                          }
                          }
                          -

                          换成了这样:

                          -
                          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);
                          }
                          +
                          // 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;
                          }
                          }
                          -

                          最后,在启动的时候,卡在了初次调度切换不到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;
                          }
                          +

                          下面是按照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
                          };
                          + +
                          编写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;
                          }
                          }
                          + +
                          修改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;
                          }
                          }
                          -
                          //in exec.c
                          // Commit to the user image.
                          oldpagetable = p->pagetable;
                          p->pagetable = pagetable;
                          +
                          在sysproc.c中添加系统调用
                          uint64
                          sys_trace(void)
                          {
                          int mask;
                          if(argint(0,&mask)<0)
                          return -1;
                          trace(mask);
                          return 0;
                          }
                          -
                          //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
                          +
                          修改fork

                          继承父进程的mask

                          +
                          np->mask = p->mask;
                          -

                          所以,我们要做的事情很简单:写一个坐收渔翁之利的函数,内容为把一个页表的所有内容复制到另一个页表。然后再在这几个地方调用这个函数即可。

                          -

                          代码

                          -

                          注意:由于我写得实在是太烦了,已经思考不下去了。为了放过我自己,我写了个虽然能过得去测试但是其实毛病重重的代码。垃圾点为以下几点:

                          -
                            -
                          1. 需要去掉freewalk中的panic

                            -

                            我的kvmcopy的实现是,user pagetable(下面简称up)和tp的相同虚拟地址共用同一页物理内存。也就是说,页表不一样,但所指向的物理内存是同一个。这样设计的目的是为了能够让tp及时用到up的更新后的数据。

                            -

                            这会导致啥呢?在进程释放时,需要一起调用proc_freepagetableproc_freekpgtblproc_freepagetable调用完后,所指向的那堆物理内存已经寄完了,如果再调用proc_freekpgtbl,显然,就会发生页表未释放但页表对应内存已经释放的问题,freewalk就会panic。因此,我简单粗暴地直接把freewalk的panic删掉了【抖】也许有别的解决方法,但我真是烦得不想想了放过我吧(

                            -
                          2. -
                          3. 好像暂时没有第二点了()

                            -
                          4. -
                          +
                          在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”.

                          -
                          渔翁之利函数
                          // 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;
                          }
                          +
                          // kernel/sysinfo.h
                          struct sysinfo {
                          uint64 freemem; // amount of free memory (bytes)
                          uint64 nproc; // number of process
                          };
                          -
                          修改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;
                          }
                          +

                          感想

                          代码

                          系统调用要做的事情同上。

                          +

                          有一个我在hit实验没想到,在这里依然没有想到的点是,参数的指针来自用户空间,所以不能直接对其指向的空间进行写入,需要借助copyout函数。

                          +

                          还有一件事,就是不知道该怎么统计free mem的数量,后来在hints提示下才知道要去kalloc.c中找。【之前只找过了vm.c】这里其实是很后悔提前看了提示的。我应该先去看一下上面关于kernel各个文件用途的笔记,再去继续自己找的,不能太过依赖提示。

                          +

                          还有一点做的不好的地方是,标答是选择了将两个计数函数放在各自的文件中,我是选择直接将成员变量在头文件中extern 公开出来,比如说在proc.h中这么写:

                          +
                          extern struct proc proc[NPROC];
                          -
                          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);
                          +

                          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;
                          }
                          -
                          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;
                          }
                          +
                          sysproc.c中
                          uint64
                          sys_sysinfo(void){
                          uint64 addr;
                          if(argaddr(0, &addr) < 0)
                          return -1;
                          return sysinfo((struct sysinfo*)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;
                          }
                          +
                          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;
                          }
                          -
                          userinit
                          -

                          这一步不能忽视,因为内核启动的时候就需要用到copyinstr。

                          -
                          -
                          // in proc.c userinit()
                          uvminit(p->pagetable, initcode, sizeof(initcode));
                          p->sz = PGSIZE;
                          // 加这个!
                          kvmcopy(p->pagetable, p->kpgtbl, p->sz);
                          +
                          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;
                          }
                          -
                          删掉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");
                          }
                          +

                          附加题

                          trace plus
                          +

                          Print the system call arguments for traced system calls.

                          +
                          +

                          这个实现起来要说简单也简单,麻烦也麻烦。这里就先摆了【实际上尝试了半小时发现太烦了看别人写的也不大满意就放弃了】

                          +
                          sysinfo plus
                          +

                          Compute the load average and export it through sysinfo

                          +
                          +

                          说实话没太看懂,不就加个 running process/ncpu就行了吗?

                          ]]> @@ -10201,698 +10131,470 @@ url访问填写http://localhost/webdemo4_war/*.do

                          由于我们的pages数组会在多个文件、多个进程间使用,所以它必须在被锁保护的区域中被使用。

                          主要难点与错误

                          scause=2

                          image-20230117161404719

                          这个发生在我还没有实现第二部分的时候。搜索了一下,scause=2为Illegal instruction,而且sepc的这个1004的值也非常诡异。这应该是因为fork子进程释放了指令段内存,导致主进程执行错误

                          -
                          kernel无法启动

                          在kinit中

                          -
                          void
                          kinit()
                          {
                          initlock(&kmem.lock, "kmem");
                          initlock(&pages_lock,"pages");
                          memset(pages, 0, (2<<19));
                          freerange(end, (void*)PHYSTOP);
                          }
                          - -

                          会通过freerange初始化freelist。在freerange中:

                          -
                          void
                          freerange(void *pa_start, void *pa_end)
                          {
                          char *p;
                          p = (char*)PGROUNDUP((uint64)pa_start);
                          for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
                          kfree(p);
                          }
                          }
                          - -

                          会对每一项进行一次kfree。因而,我们需要在kfree前先增加一次引用,要不然会寄。

                          -
                          在缺页中断时减少对物理页的引用数

                          image-20230117213903706

                          -

                          注意此处不能直接让pages[pa/PGSIZE]–,一定要借助kfree。当此进程为引用pa的最后一个进程的时候,如果仅减少引用数,就会造成内存泄漏。kfree可以既减少引用数,又在适当的时候对物理页释放,可谓一举两得。kfree的这个双重作用思想也在uvmunmap中体现了。

                          -
                          在内核态中引发并处理缺页中断
                          -

                          Modify copyout() to use the same scheme as page faults when it encounters a COW page.

                          -
                          -

                          我们所做的第一第二部分仅仅是完成了对来自用户态的缺页中断的完美处理,还尚未处理来自内核态的缺页中断。因而,这个修改copyin和copyout的点实际上就是要我们处理内核态的缺页中断。

                          -

                          这次实验跟上次的lazy allocation一样,都可以直接在walkaddr进行特殊处理,并且差不多要把usertrap的全部代码挪过来【具体见lazy allocation的代码】。不过,我想出了另一个流氓的方法(也就是说其实原理感觉是不大对233)。我选择直接在kernel引发一个访问用户页面的缺页中断,然后在kerneltrap中处理这个中断,就像usertrap一样。

                          -

                          但由于在walkaddr中发生的中断处于内核状态下,所以就进不了usertrap。我们应该在kerneltrap中再次添加和usertrap一样的中断处理。我们会像这样引发一个中断:

                          -
                          if((*pte & PTE_COW) != 0){
                          if((*pte & PTE_W) == 0){
                          *(char*)va = 1;// 此处不能用pa哦
                          - -

                          然后在kerneltrap中这样处理:

                          -
                          if(r_scause() == 15){
                          // 只要写入引起的缺页中断
                          uint64 va = r_stval();
                          pte_t *pte;
                          uint64 pa;
                          uint flags;

                          if((pte = walk(p->pagetable, va, 0)) == 0)
                          p->killed = 1;
                          else if((*pte & PTE_V) == 0)
                          p->killed = 1;
                          else {
                          sepc += 4;
                          pa = PTE2PA(*pte);
                          flags = PTE_FLAGS(*pte);

                          char* mem;
                          if((mem = kalloc())!=0){
                          memmove(mem, (char*)pa, PGSIZE);
                          // 设置为新的物理页地址
                          *pte = PA2PTE(mem);
                          kfree((void*)pa);
                          // 设置新的flag,标记为可写
                          flags = (flags | PTE_W | PTE_COW);
                          *pte = ((*pte) | flags);
                          } else{
                          p->killed = 1;
                          }
                          }
                          } else if((which_dev = devintr()) == 0){
                          - -

                          但是,这样做是不行的。

                          -
                          -

                          会在这里卡住,会无限次不断进入kerneltrap。

                          -

                          image-20230117235028133

                          -
                          -

                          造成这个的原因,经过一番曲折的debug之后,我发现,只要像usertrap中的syscall分支一样:

                          -
                          if(r_scause() == 8){
                          // system call

                          // 重点是这里
                          // sepc points to the ecall instruction,
                          // but we want to return to the next instruction.
                          p->trapframe->epc += 4;
                          // ...
                          }
                          - -

                          加上这句话就行:

                          -
                          sepc += 4;
                          - -

                          所以,结果就非常显而易见了,是因为一直卡在这句话执行不下去:

                          -
                          *(char*)va = 1;
                          - -

                          这是因为缺页中断会返回到原代码句中执行,所以就会继续回到这句话。而我们知道,此时正处于内核态,并没有开启地址映射,所以此处其实是非法地址越界了。但是我们的目的确实已经达到了(因为va通过stval寄存器被传递到了kerneltrap),所以我们这里只需跳过这句即可。

                          -

                          这也是为什么说我这个方法虽然实现了,但本质上其实非常流氓,不算是kerneltrap。

                          -
                          -

                          Update:验收的时候跟学长说了一下这个点,学长表示不算流氓,反而在内核(至少内核赛)中算是一个比较通用的手法hh没想到还误打误撞上了

                          -

                          它带来的好处是,当地址不合法的时候可以减少开销。

                          -

                          具体来说,内核中一般会将地址空间分为多个vma,因而检查地址越界无需像xv6那样简单查页表,只需查地址是否在对应的vma中即可。所以,直接把这东西转到一个硬件的缺页中断中实现,事实上确实是减少了地址非法时的开销。

                          -
                          -

                          除了这一点外,还有一点很重要的是,由于walkaddr是需要返回一个pa的,因而我们需要手动再把pa在缺页中断后更新一下:

                          -
                          pa = PTE2PA(*pte);
                          if((*pte & PTE_COW) != 0){
                          if((*pte & PTE_W) == 0){
                          *(char*)va = 1;
                          pa = PTE2PA(*pte);
                          }
                          }
                          return pa;
                          - -

                          总之,做了这两个关键步骤后,也能启动了,也能过cowtest了。所以下面的代码也就贴上了这里的版本。

                          -

                          心得

                          本次实验耗时经典五小时(包含笔记时间就是六个半小时了hhh),算是平均水平。很遗憾也很难受的一点是,我的错误最终还是没有自己想出来,而是参考了别人的代码才改对的。思路很简单,但是细节也依然非常多非常坑,还是得再加把劲。

                          -

                          代码

                          我们只需要在fork的时候,标记父子进程的所有PTE都为read-only,然后之后就会遇到不同scause的缺页中断,针对特定的scause,新建物理页面,拷贝物理页面,然后重新设置映射即可。而对于其提出的需要标记某页是否能够释放,则需要统计每页的ref数,当ref==1的时候才可以释放。

                          -

                          定义COW标记

                          // in kernel/riscv.h
                          #define PTE_V (1L << 0)
                          // ...
                          #define PTE_COW (1L << 5)
                          - -

                          引用数组初始化

                          // in kernel/kalloc.c
                          char pages[(2<<19)];
                          struct spinlock pages_lock;

                          void
                          kinit()
                          {
                          initlock(&kmem.lock, "kmem");
                          initlock(&pages_lock,"pages");
                          memset(pages, 0, (2<<19));
                          freerange(end, (void*)PHYSTOP);
                          }
                          void
                          freerange(void *pa_start, void *pa_end)
                          {
                          char *p;
                          p = (char*)PGROUNDUP((uint64)pa_start);
                          for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
                          pages[(uint64)p/PGSIZE] = 1;
                          kfree(p);
                          }
                          }
                          - -

                          申请和释放页时增删引用

                          // in kernel/kalloc.c
                          void *
                          kalloc(void)
                          {
                          struct run *r;

                          acquire(&kmem.lock);
                          r = kmem.freelist;
                          if(r)
                          kmem.freelist = r->next;
                          release(&kmem.lock);
                          if(r){
                          acquire(&pages_lock);
                          // 在这
                          pages[(uint64)r/PGSIZE] = 1;
                          release(&pages_lock);
                          }
                          if(r)
                          memset((char*)r, 5, PGSIZE); // fill with junk
                          return (void*)r;
                          }
                          void
                          kfree(void *pa)
                          {
                          struct run *r;

                          acquire(&pages_lock);
                          if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP || pages[(uint64)pa/PGSIZE] <= 0 )
                          panic("kfree");
                          // 每次kfree都会减少引用
                          pages[(uint64)pa/PGSIZE]--;
                          // 说明此时页面还被其他东西引用着,不能释放
                          if(pages[((uint64)pa)/PGSIZE] > 0){
                          release(&pages_lock);
                          return;
                          }
                          release(&pages_lock);

                          // Fill with junk to catch dangling refs.
                          memset(pa, 1, PGSIZE);
                          r = (struct run*)pa;

                          acquire(&kmem.lock);
                          r->next = kmem.freelist;
                          kmem.freelist = r;
                          release(&kmem.lock);
                          }
                          - -

                          修改fork时对页表的复制操作,并标记引用数增加

                          // in kernel/proc.c fork()
                          // Copy user memory from parent to child.
                          if(cowcopy(p->pagetable, np->pagetable, p->sz) < 0){
                          freeproc(np);
                          release(&np->lock);
                          return -1;
                          }
                          // in kernel/vm.c
                          int
                          cowcopy(pagetable_t old, pagetable_t new, uint64 sz)
                          {
                          pte_t *pte;
                          uint64 pa, i;
                          uint flags;

                          for(i = 0; i < sz; i += PGSIZE){
                          if((pte = walk(old, i, 0)) == 0)
                          panic("cowcopy: pte should exist");
                          if((*pte & PTE_V) == 0)
                          panic("cowcopy: page not present");
                          pa = PTE2PA(*pte);
                          flags = PTE_FLAGS(*pte);
                          // 去除flag中的PTE_W,并且给父子的都安上没有PTE_W的flag
                          flags = (flags & (~PTE_W));
                          flags = (flags | PTE_COW);
                          *pte = ((*pte) & (~PTE_W));
                          *pte = ((*pte) | PTE_COW);
                          if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
                          goto err;
                          }
                          // 标记物理页的引用数增加
                          acquire(&pages_lock);
                          pages[pa/PGSIZE]++;
                          release(&pages_lock);
                          }
                          return 0;
                          err:
                          // 失败了不能释放物理内存
                          uvmunmap(new, 0, i / PGSIZE, 0);
                          return -1;
                          }
                          - -

                          处理缺页中断,标记引用数减少

                          } else if(r_scause() == 15){
                          // 只要求写入引起的缺页中断
                          uint64 va = r_stval();
                          pte_t *pte;
                          uint64 pa;
                          uint flags;

                          if((pte = walk(p->pagetable, va, 0)) == 0)
                          p->killed = 1;
                          else if((*pte & PTE_V) == 0)
                          p->killed = 1;
                          else {
                          pa = PTE2PA(*pte);
                          flags = PTE_FLAGS(*pte);

                          char* mem;
                          if((mem = kalloc())!=0){
                          memmove(mem, (char*)pa, PGSIZE);
                          // 设置为新的物理页地址
                          *pte = PA2PTE(mem);
                          // 减少引用,引用归零时释放
                          kfree((void*)pa);
                          // 设置新的flag,标记为可写
                          flags = (flags | PTE_W | PTE_COW);
                          *pte = ((*pte) | flags);
                          } else{
                          p->killed = 1;
                          }
                          }
                          }
                          - -

                          uvmunmap时减少引用数

                          void
                          uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
                          {
                          uint64 a;
                          pte_t *pte;
                          // ...

                          for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
                          // ...
                          uint64 pa = PTE2PA(*pte);
                          if(do_free){
                          kfree((void*)pa);
                          }else {
                          acquire(&pages_lock);
                          if(((*pte) & PTE_COW) != 0){
                          pages[pa/PGSIZE]--;
                          }
                          release(&pages_lock);
                          }
                          *pte = 0;
                          }
                          }
                          - -

                          修改walkaddr

                          在walkaddr中触发缺页中断
                          uint64
                          walkaddr(pagetable_t pagetable, uint64 va)
                          {
                          pte_t *pte;
                          uint64 pa;

                          if(va >= MAXVA)
                          return 0;

                          pte = walk(pagetable, va, 0);
                          if(pte == 0)
                          return 0;
                          if((*pte & PTE_V) == 0)
                          return 0;
                          if((*pte & PTE_U) == 0)
                          return 0;
                          pa = PTE2PA(*pte);
                          // 在这里
                          if((*pte & PTE_COW) != 0){
                          if((*pte & PTE_W) == 0){
                          // 触发缺页中断
                          *(char*)va = 1;
                          // 更新pa值
                          pa = PTE2PA(*pte);
                          }
                          }
                          return pa;
                          }
                          - -
                          在kerneltrap内补上对缺页中断的处理
                          void
                          kerneltrap()
                          {
                          uint64 sepc = r_sepc();
                          // ...
                          if(r_scause() == 15){
                          // 只要写入引起的缺页中断
                          uint64 va = r_stval();
                          pte_t *pte;
                          uint64 pa;
                          uint flags;

                          if((pte = walk(p->pagetable, va, 0)) == 0)
                          p->killed = 1;
                          else if((*pte & PTE_V) == 0)
                          p->killed = 1;
                          else {
                          // 注意,这个很重要!!!!!
                          sepc += 4;
                          pa = PTE2PA(*pte);
                          flags = PTE_FLAGS(*pte);

                          char* mem;
                          if((mem = kalloc())!=0){
                          memmove(mem, (char*)pa, PGSIZE);
                          // 设置为新的物理页地址
                          *pte = PA2PTE(mem);
                          kfree((void*)pa);
                          // 设置新的flag,标记为可写
                          flags = (flags | PTE_W | PTE_COW);
                          *pte = ((*pte) | flags);
                          } else{
                          p->killed = 1;
                          }
                          }
                          } else if((which_dev = devintr()) == 0){
                          printf("scause %p\n", scause);
                          printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
                          panic("kerneltrap");
                          }
                          // ...
                          }
                          - -]]> -
                          - - 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");
                          }
                          - -

                          这是一个利用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);
                          }
                          - -

                          也即父进程创建出子进程来执行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).

                          -
                          -

                          句柄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);
                          }
                          }
                          - -

                          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);
                          }
                          - -

                          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);
                          }
                          +
                          kernel无法启动

                          在kinit中

                          +
                          void
                          kinit()
                          {
                          initlock(&kmem.lock, "kmem");
                          initlock(&pages_lock,"pages");
                          memset(pages, 0, (2<<19));
                          freerange(end, (void*)PHYSTOP);
                          }
                          -

                          dup

                          dup系统调用复制一个现有的文件描述符,返回一个引用自同一个底层I/O对象的新文件描述符。

                          -

                          dup和open一样,都是会占用一个新的句柄的,而且都是优先分配数值小的。比如说fd = dup(3),得到fd=4,那么结果就是句柄3和句柄4指向同一个文件,并且偏移量一样。

                          -

                          dup可以让这样的指令变得可以实现:

                          -
                          ls existing-file non-existing-file > tmp1 2>&1
                          +

                          会通过freerange初始化freelist。在freerange中:

                          +
                          void
                          freerange(void *pa_start, void *pa_end)
                          {
                          char *p;
                          p = (char*)PGROUNDUP((uint64)pa_start);
                          for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
                          kfree(p);
                          }
                          }
                          -
                          -

                          这个指令的意思是,先把stderr的结果重定向到stdout,再把stdout的结果重定向到tmp1中。

                          -

                          关于2>&1的解释,可以看这个 shell中的”2>&1”是什么意思?

                          +

                          会对每一项进行一次kfree。因而,我们需要在kfree前先增加一次引用,要不然会寄。

                          +
                          在缺页中断时减少对物理页的引用数

                          image-20230117213903706

                          +

                          注意此处不能直接让pages[pa/PGSIZE]–,一定要借助kfree。当此进程为引用pa的最后一个进程的时候,如果仅减少引用数,就会造成内存泄漏。kfree可以既减少引用数,又在适当的时候对物理页释放,可谓一举两得。kfree的这个双重作用思想也在uvmunmap中体现了。

                          +
                          在内核态中引发并处理缺页中断
                          +

                          Modify copyout() to use the same scheme as page faults when it encounters a COW page.

                          -

                          这个的实现就要用到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]);
                          }
                          +

                          我们所做的第一第二部分仅仅是完成了对来自用户态的缺页中断的完美处理,还尚未处理来自内核态的缺页中断。因而,这个修改copyin和copyout的点实际上就是要我们处理内核态的缺页中断。

                          +

                          这次实验跟上次的lazy allocation一样,都可以直接在walkaddr进行特殊处理,并且差不多要把usertrap的全部代码挪过来【具体见lazy allocation的代码】。不过,我想出了另一个流氓的方法(也就是说其实原理感觉是不大对233)。我选择直接在kernel引发一个访问用户页面的缺页中断,然后在kerneltrap中处理这个中断,就像usertrap一样。

                          +

                          但由于在walkaddr中发生的中断处于内核状态下,所以就进不了usertrap。我们应该在kerneltrap中再次添加和usertrap一样的中断处理。我们会像这样引发一个中断:

                          +
                          if((*pte & PTE_COW) != 0){
                          if((*pte & PTE_W) == 0){
                          *(char*)va = 1;// 此处不能用pa哦
                          -

                          完成了父进程-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;
                          +

                          然后在kerneltrap中这样处理:

                          +
                          if(r_scause() == 15){
                          // 只要写入引起的缺页中断
                          uint64 va = r_stval();
                          pte_t *pte;
                          uint64 pa;
                          uint flags;

                          if((pte = walk(p->pagetable, va, 0)) == 0)
                          p->killed = 1;
                          else if((*pte & PTE_V) == 0)
                          p->killed = 1;
                          else {
                          sepc += 4;
                          pa = PTE2PA(*pte);
                          flags = PTE_FLAGS(*pte);

                          char* mem;
                          if((mem = kalloc())!=0){
                          memmove(mem, (char*)pa, PGSIZE);
                          // 设置为新的物理页地址
                          *pte = PA2PTE(mem);
                          kfree((void*)pa);
                          // 设置新的flag,标记为可写
                          flags = (flags | PTE_W | PTE_COW);
                          *pte = ((*pte) | flags);
                          } else{
                          p->killed = 1;
                          }
                          }
                          } else if((which_dev = devintr()) == 0){
                          -

                          这实际上是二叉树的左右中递归过程。

                          +

                          但是,这样做是不行的。

                          -

                          附:对于管道命令的解读

                          -
                          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
                          - -

                          这个命令最后的结果是,秒速出hi,然后等待10s后结束,了。由于echo的输出与stdin没有关系,所以,echo不会阻塞读入stdin,等待管道关闭,而是会即刻输出hi。

                          +

                          会在这里卡住,会无限次不断进入kerneltrap。

                          +

                          image-20230117235028133

                          -

                          管道实际上就相当于:

                          -
                          echo hello world | wc
                          echo hello world > /tmp/xyz; wc < /tmp/xyz
                          +

                          造成这个的原因,经过一番曲折的debug之后,我发现,只要像usertrap中的syscall分支一样:

                          +
                          if(r_scause() == 8){
                          // system call

                          // 重点是这里
                          // sepc points to the ecall instruction,
                          // but we want to return to the next instruction.
                          p->trapframe->epc += 4;
                          // ...
                          }
                          -

                          在这种情况下,管道相比临时文件至少有四个优势

                          -
                            -
                          • 首先,不用删文件
                          • -
                          • 其次,管道可以任意传递长的数据流
                          • -
                          • 第三,管道允许一定程度上的并行
                          • -
                          • 第四,如果实现进程间通讯,管道的块读写比文件的非块语义更有效率。
                          • -
                          -

                          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 
                          +

                          加上这句话就行:

                          +
                          sepc += 4;
                          -

                          测试安装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
                          ...
                          +

                          所以,结果就非常显而易见了,是因为一直卡在这句话执行不下去:

                          +
                          *(char*)va = 1;
                          +

                          这是因为缺页中断会返回到原代码句中执行,所以就会继续回到这句话。而我们知道,此时正处于内核态,并没有开启地址映射,所以此处其实是非法地址越界了。但是我们的目的确实已经达到了(因为va通过stval寄存器被传递到了kerneltrap),所以我们这里只需跳过这句即可。

                          +

                          这也是为什么说我这个方法虽然实现了,但本质上其实非常流氓,不算是kerneltrap。

                          -

                          注,这里出现了一个问题,qemu-system-riscv64 --version打出来发现qemu-system-riscv64 command not found。似乎是我的ubuntu16.04版本太低了【悲】去看了下网上,可以按照这个来做:

                          -

                          rCore qemu risc-v 实验环境配置

                          +

                          Update:验收的时候跟学长说了一下这个点,学长表示不算流氓,反而在内核(至少内核赛)中算是一个比较通用的手法hh没想到还误打误撞上了

                          +

                          它带来的好处是,当地址不合法的时候可以减少开销。

                          +

                          具体来说,内核中一般会将地址空间分为多个vma,因而检查地址越界无需像xv6那样简单查页表,只需查地址是否在对应的vma中即可。所以,直接把这东西转到一个硬件的缺页中断中实现,事实上确实是减少了地址非法时的开销。

                          -

                          下载编译xv6源码

                          随后,进入一个你喜欢的文件夹clone xv6的实验源码,输入

                          -
                          $ git clone git://g.csail.mit.edu/xv6-labs-2020
                          $ cd xv6-labs-2020
                          $ git checkout util
                          +

                          除了这一点外,还有一点很重要的是,由于walkaddr是需要返回一个pa的,因而我们需要手动再把pa在缺页中断后更新一下:

                          +
                          pa = PTE2PA(*pte);
                          if((*pte & PTE_COW) != 0){
                          if((*pte & PTE_W) == 0){
                          *(char*)va = 1;
                          pa = PTE2PA(*pte);
                          }
                          }
                          return pa;
                          -

                          然后进行编译

                          -
                          $ make
                          $ make qemu
                          +

                          总之,做了这两个关键步骤后,也能启动了,也能过cowtest了。所以下面的代码也就贴上了这里的版本。

                          +

                          心得

                          本次实验耗时经典五小时(包含笔记时间就是六个半小时了hhh),算是平均水平。很遗憾也很难受的一点是,我的错误最终还是没有自己想出来,而是参考了别人的代码才改对的。思路很简单,但是细节也依然非常多非常坑,还是得再加把劲。

                          +

                          代码

                          我们只需要在fork的时候,标记父子进程的所有PTE都为read-only,然后之后就会遇到不同scause的缺页中断,针对特定的scause,新建物理页面,拷贝物理页面,然后重新设置映射即可。而对于其提出的需要标记某页是否能够释放,则需要统计每页的ref数,当ref==1的时候才可以释放。

                          +

                          定义COW标记

                          // in kernel/riscv.h
                          #define PTE_V (1L << 0)
                          // ...
                          #define PTE_COW (1L << 5)
                          -

                          如果此处发生错误: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
                          +

                          引用数组初始化

                          // in kernel/kalloc.c
                          char pages[(2<<19)];
                          struct spinlock pages_lock;

                          void
                          kinit()
                          {
                          initlock(&kmem.lock, "kmem");
                          initlock(&pages_lock,"pages");
                          memset(pages, 0, (2<<19));
                          freerange(end, (void*)PHYSTOP);
                          }
                          void
                          freerange(void *pa_start, void *pa_end)
                          {
                          char *p;
                          p = (char*)PGROUNDUP((uint64)pa_start);
                          for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
                          pages[(uint64)p/PGSIZE] = 1;
                          kfree(p);
                          }
                          }
                          -

                          再执行一次

                          -
                          $ make
                          $ make qemu
                          +

                          申请和释放页时增删引用

                          // in kernel/kalloc.c
                          void *
                          kalloc(void)
                          {
                          struct run *r;

                          acquire(&kmem.lock);
                          r = kmem.freelist;
                          if(r)
                          kmem.freelist = r->next;
                          release(&kmem.lock);
                          if(r){
                          acquire(&pages_lock);
                          // 在这
                          pages[(uint64)r/PGSIZE] = 1;
                          release(&pages_lock);
                          }
                          if(r)
                          memset((char*)r, 5, PGSIZE); // fill with junk
                          return (void*)r;
                          }
                          void
                          kfree(void *pa)
                          {
                          struct run *r;

                          acquire(&pages_lock);
                          if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP || pages[(uint64)pa/PGSIZE] <= 0 )
                          panic("kfree");
                          // 每次kfree都会减少引用
                          pages[(uint64)pa/PGSIZE]--;
                          // 说明此时页面还被其他东西引用着,不能释放
                          if(pages[((uint64)pa)/PGSIZE] > 0){
                          release(&pages_lock);
                          return;
                          }
                          release(&pages_lock);

                          // Fill with junk to catch dangling refs.
                          memset(pa, 1, PGSIZE);
                          r = (struct run*)pa;

                          acquire(&kmem.lock);
                          r->next = kmem.freelist;
                          kmem.freelist = r;
                          release(&kmem.lock);
                          }
                          -

                          就ok了。

                          -

                          关闭qemu

                          qemu退出操作

                          -

                          在这里记个强制方法:

                          -
                          ps -elf | grep qemu
                          +

                          修改fork时对页表的复制操作,并标记引用数增加

                          // in kernel/proc.c fork()
                          // Copy user memory from parent to child.
                          if(cowcopy(p->pagetable, np->pagetable, p->sz) < 0){
                          freeproc(np);
                          release(&np->lock);
                          return -1;
                          }
                          // in kernel/vm.c
                          int
                          cowcopy(pagetable_t old, pagetable_t new, uint64 sz)
                          {
                          pte_t *pte;
                          uint64 pa, i;
                          uint flags;

                          for(i = 0; i < sz; i += PGSIZE){
                          if((pte = walk(old, i, 0)) == 0)
                          panic("cowcopy: pte should exist");
                          if((*pte & PTE_V) == 0)
                          panic("cowcopy: page not present");
                          pa = PTE2PA(*pte);
                          flags = PTE_FLAGS(*pte);
                          // 去除flag中的PTE_W,并且给父子的都安上没有PTE_W的flag
                          flags = (flags & (~PTE_W));
                          flags = (flags | PTE_COW);
                          *pte = ((*pte) & (~PTE_W));
                          *pte = ((*pte) | PTE_COW);
                          if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
                          goto err;
                          }
                          // 标记物理页的引用数增加
                          acquire(&pages_lock);
                          pages[pa/PGSIZE]++;
                          release(&pages_lock);
                          }
                          return 0;
                          err:
                          // 失败了不能释放物理内存
                          uvmunmap(new, 0, i / PGSIZE, 0);
                          return -1;
                          }
                          -

                          image-20230105153458808

                          -

                          记住第二个的pid

                          -

                          然后

                          -
                          kill 3303
                          +

                          处理缺页中断,标记引用数减少

                          } else if(r_scause() == 15){
                          // 只要求写入引起的缺页中断
                          uint64 va = r_stval();
                          pte_t *pte;
                          uint64 pa;
                          uint flags;

                          if((pte = walk(p->pagetable, va, 0)) == 0)
                          p->killed = 1;
                          else if((*pte & PTE_V) == 0)
                          p->killed = 1;
                          else {
                          pa = PTE2PA(*pte);
                          flags = PTE_FLAGS(*pte);

                          char* mem;
                          if((mem = kalloc())!=0){
                          memmove(mem, (char*)pa, PGSIZE);
                          // 设置为新的物理页地址
                          *pte = PA2PTE(mem);
                          // 减少引用,引用归零时释放
                          kfree((void*)pa);
                          // 设置新的flag,标记为可写
                          flags = (flags | PTE_W | PTE_COW);
                          *pte = ((*pte) | flags);
                          } else{
                          p->killed = 1;
                          }
                          }
                          }
                          -

                          测试gdb是否ok

                          见该文章最后一部分

                          -

                          【MIT6.S081/6.828】手把手教你搭建开发环境

                          -

                          自测方法

                          make grade
                          +

                          uvmunmap时减少引用数

                          void
                          uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
                          {
                          uint64 a;
                          pte_t *pte;
                          // ...

                          for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
                          // ...
                          uint64 pa = PTE2PA(*pte);
                          if(do_free){
                          kfree((void*)pa);
                          }else {
                          acquire(&pages_lock);
                          if(((*pte) & PTE_COW) != 0){
                          pages[pa/PGSIZE]--;
                          }
                          release(&pages_lock);
                          }
                          *pte = 0;
                          }
                          }
                          -

                          或者如果只想测其中一个,可以:

                          -
                          ./grade-lab-util sleep
                          +

                          修改walkaddr

                          在walkaddr中触发缺页中断
                          uint64
                          walkaddr(pagetable_t pagetable, uint64 va)
                          {
                          pte_t *pte;
                          uint64 pa;

                          if(va >= MAXVA)
                          return 0;

                          pte = walk(pagetable, va, 0);
                          if(pte == 0)
                          return 0;
                          if((*pte & PTE_V) == 0)
                          return 0;
                          if((*pte & PTE_U) == 0)
                          return 0;
                          pa = PTE2PA(*pte);
                          // 在这里
                          if((*pte & PTE_COW) != 0){
                          if((*pte & PTE_W) == 0){
                          // 触发缺页中断
                          *(char*)va = 1;
                          // 更新pa值
                          pa = PTE2PA(*pte);
                          }
                          }
                          return pa;
                          }
                          -

                          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

                          +
                          在kerneltrap内补上对缺页中断的处理
                          void
                          kerneltrap()
                          {
                          uint64 sepc = r_sepc();
                          // ...
                          if(r_scause() == 15){
                          // 只要写入引起的缺页中断
                          uint64 va = r_stval();
                          pte_t *pte;
                          uint64 pa;
                          uint flags;

                          if((pte = walk(p->pagetable, va, 0)) == 0)
                          p->killed = 1;
                          else if((*pte & PTE_V) == 0)
                          p->killed = 1;
                          else {
                          // 注意,这个很重要!!!!!
                          sepc += 4;
                          pa = PTE2PA(*pte);
                          flags = PTE_FLAGS(*pte);

                          char* mem;
                          if((mem = kalloc())!=0){
                          memmove(mem, (char*)pa, PGSIZE);
                          // 设置为新的物理页地址
                          *pte = PA2PTE(mem);
                          kfree((void*)pa);
                          // 设置新的flag,标记为可写
                          flags = (flags | PTE_W | PTE_COW);
                          *pte = ((*pte) | flags);
                          } else{
                          p->killed = 1;
                          }
                          }
                          } else if((which_dev = devintr()) == 0){
                          printf("scause %p\n", scause);
                          printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
                          panic("kerneltrap");
                          }
                          // ...
                          }
                          + +]]> + + + 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行左右。

                          -
                          体会
                          参数

                          注意,他要求我们实现的sleep的参数是ticks的数量,不是秒数。我花了半天找时钟周期大小这个参数在哪,找了许久没找到,估计是没考虑到这一点。

                          -

                          比如说,我翻了一下linux0.11的源码,在include/linux/time.h下有这句:

                          -

                          image-20230105162505574.png

                          -

                          说明了时钟频率大小。在xv6好像没有看到对这个的显式说明。

                          -
                          系统调用过程

                          感受了一下xv6的系统调用过程,跟linux0.11还是很相像的。

                          -

                          这个好像是lab2的内容,我暂且先在此放下我体会到的感受。

                          +

                          虚拟地址有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页。

                          +

                          三级页表结构相比于单级页表结构,可以节省更多内存空间

                          +
                          +

                          参考:页表是啥以及为啥多级页表能够节省空间

                          +
                          +

                          考虑到这样一个进程:

                          +

                          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的对比

                          +
                          +

                          每个进程都有一个用户级别的页表。xv6给内核提供了一个单独的内核地址空间的页表。其层级映射关系如下:

                          +

                          p3

                          +

                          在kernel/memlayout.h中正记录了这些参数:

                          +
                          // Physical memory layout

                          // qemu -machine virt is set up like this,
                          // based on qemu's hw/riscv/virt.c:
                          //
                          // 00001000 -- boot ROM, provided by qemu
                          // 02000000 -- CLINT
                          // 0C000000 -- PLIC
                          // 10000000 -- uart0
                          // 10001000 -- virtio disk
                          // 80000000 -- boot ROM jumps here in machine mode
                          // -kernel loads the kernel here
                          // unused RAM after 80000000.

                          // the kernel uses physical memory thus:
                          // 80000000 -- entry.S, then kernel text and data
                          // end -- start of kernel page allocation area
                          // PHYSTOP -- end RAM used by the kernel

                          // qemu puts UART registers here in physical memory.
                          #define UART0 0x10000000L
                          #define UART0_IRQ 10

                          // virtio mmio interface
                          #define VIRTIO0 0x10001000
                          #define VIRTIO0_IRQ 1

                          // core local interruptor (CLINT), which contains the timer.
                          // ...
                          + +

                          由图可知,一直从0x0到0x86400000,都是采取的直接映射的方式,虚拟地址=物理地址,这段是内核使用的空间。在0x0-0x800000000阶段,物理地址代表着各种IO设备的存储器。

                          +

                          但是注意,在0x86400000(PHYSTOP)以上的地址都不是直接映射,这些非直接映射的层级包含两类:

                            -
                          1. 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. trampoline

                            +
                            +

                            It is mapped at the top of the virtual address space; user page tables have this same mapping.

                            +
                            +

                            它有一点很特殊的是,它实际对应的物理内存是0x80000000开始的一段。也就是说,0x80000000开始的这段内存,既被直接映射了,也被trampoline通过虚拟地址映射了。它被映射了两次。

                          3. -
                          4. linux0.11

                            -

                            首先是用户态到内核态的切换。

                            -

                            在用户态中比方说调用system call close(),则会调用lib/close.c下的:

                            -

                            image-20230105173820813

                            -

                            展开这个宏之后,是这样的:

                            -

                            image-20230105173845317

                            -

                            具体意思就是把close的系统调用号存入参数寄存器,然后引发0x80中断,进入内核态。

                            -

                            然后是在内核态的执行。

                            -

                            查表会得知sys_call函数是0x80中断的中断处理函数,然后就会根据参数里的系统调用名字去找系统调用表执行

                            -

                            image-20230105174832400

                            -

                            这部分跟xv6差不多,不再赘述

                            +
                          5. 内核栈

                            +
                            +

                            Each process has its own kernel stack, which is mapped high so that below it xv6 can leave an unmapped guard page. The guard page’s PTE is invalid (i.e., PTE_V is not set), so that if the kernel overflflows a kernel stack, it will likely cause an exception and the kernel will panic.

                            +

                            guard page可以用来防止内核栈溢出。

                            +
                          -

                          可见,这两个系统在内核态的实现是差不多的,只是在用户态有点稍稍不一样。感觉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());
                          +

                          内核使用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;
                          }
                          -

                          这下是真的尴尬了23333

                          -

                          但总而言之,自己写了那俩不够优雅的函数还算是有点用【大概】。以下是我的代码

                          -

                          编写primes

                          -

                          参考:

                          -

                          MIT操作系统实验lab1(案例:primes(质数筛选)附代码、详解)

                          -

                          XV6实验-Lab0 Utilities

                          -
                          +

                          其中,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");
                          }
                          + +

                          实现主要逻辑的是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

                          在内核运行的时候,需要申请很多空间用来存放各种数据。

                          -

                          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.

                          +

                          The kernel must allocate and free physical memory at run-time for page tables, user memory, kernel stacks, and pipe buffers.

                          -

                          其实就是用生产者消费者模式来写素数计算的并发版本,这个我熟

                          -

                          ……以上是第一印象。然后我看着超链接文章里的素数筛的图片,以及指导书给的提示:

                          +

                          用的是这段空闲内存:

                          +

                          image-20230109225700837

                          -

                          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.
                          • -
                          +

                          It keeps track of which pages are free by threading a linked list through the pages themselves.

                          -

                          义无反顾地……使用了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.

                          +

                          kalloc.c中就是这么实现的。

                          +

                          Code: Physical memory allocator

                          内核运行时申请释放空闲物理空间是通过kernel/kalloc.c完成的。它为内核栈、用户进程、页表和管道buffer服务。

                          +
                          +

                          kalloc.c用来在运行时申请分配新的一页,上面的vm.c正是用了kalloc申请一页,要么作为页表,要么作为存储数据的第三级页表指向的物理内存。

                          -
                          初始版

                          直接照着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.

                          +

                          最后应该会在空闲内存内形成这样的结构:

                          +

                          内存分成一页一页的,每页内存中的前几个字节存储着其对应队列中下一块内存的物理地址。不一定是从小地址到大地址顺序连接。

                          +
                          +

                          It store each free page’s run structure in the free page itself, since there’s nothing else stored there.

                          -
                          体会

                          思路还是很直观的,就是从stdin一行一行读入数据,然后把这数据处理成参数,最后调用exec就行。就是中间有很多小细节值得注意。

                          -

                          有一点比较坑的是,main方法的那个argc的计算方法是这样的,不是直接用数组的长度:

                          -
                          for(argc = 0; argv[argc]; argc++) 
                          +
                          // 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;
                          }
                          -

                          可以看到,合格的argv的形式应该是:参1 参2 参3 “\0”,最后一个元素要以”\0”标志结束。

                          -

                          这个应该是编写者约定俗成的。在user/sh.c的parseexec,大概445行左右:

                          -

                          image-20230106172133338

                          -

                          shell处理命令时是会默认把最后一个清零的。

                          +

                          Process address space

                          当用户进程叫xv6分配内存时,xv6会用kalloc去取,然后登记在页表上。

                          -

                          确实,后面在学内存的时候,用户空间的构成如图所示:

                          -

                          image-20230109234930690

                          -

                          可以看到栈那边,参数列完了之后是会有一个用以terminate的空指针的

                          +

                          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.

                          -

                          附加题:改善shell

                          看起来又难又多所以我先摸了【润】等之后有时间再回来弄吧

                          -]]> - - - 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后的部分永远无法得到执行。

                          +

                          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.

                          -

                          Real world

                          -

                          大多数操作系统都支持POSIX线程(Pthread),它允许一个用户进程在不同的CPU上同时运行几个线程。Pthread支持用户级锁(user-level locks)、障碍(barriers)等。支持Pthread需要操作系统的支持。例如,如果一个Pthread在系统调用中阻塞,同一进程的另一个Pthread应当能够在该CPU上运行。另一个例子是,如果一个线程改变了其进程的地址空间(例如,映射或取消映射内存),内核必须安排运行同一进程下的线程的其他CPU更新其硬件页表,以反映地址空间的变化。

                          +

                          感想

                          实验内容:

                          +

                          实现void pgaccess(uint64 sva,int pgnum,int* bitmask);,一个系统调用。在这里面,我们要做的是,访问从svasva+pgnum*PGSIZE这一范围内的虚拟地址对应的PTE,然后查看PTE的标记项是否有PTE_A。有的话则在bitmask对应位标记为1.

                          +

                          应该注意的点:

                          +

                          1.需要进行内核态到用户态的参数传递 2.需要进行系统调用的必要步骤 3.PTE_A需要自己定义

                          +

                          以上是初见。做完了发现,确实就是那么简单,我主要时间花费在下的实验版本不对,折腾来折腾去了可能有一个小时,最后还是选择了直接把测试函数搬过来手工调用。已经换到正确的年份版本了【泪目】

                          +

                          有一点我忽视了,看了提示才知道:

                          +
                          +

                          Be sure to clear PTE_A after checking if it is set. Otherwise, it won’t be possible to determine if the page was accessed since the last time pgaccess() was called (i.e., the bit will be set forever).

                          -

                          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.

                          +

                          也就是说每次检查到一个,就需要手动清除掉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);
                          }
                          + +
                          // 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;
                          }
                          + +

                          A kernel page table per process

                          +

                          The goal of this section and the next is to allow the kernel to directly dereference user pointers.

                          -

                          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 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.

                          -

                          感想

                          总之,意思就是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。

                          -

                          第四步的时候我是一次窃取一页。我看到一个一次窃取多页的做法,我觉得很有想法,在这里附上链接:

                          +

                          感想

                          这个其实平心而论不难,思路很简单。写着不难是不难,但想明白花费了我很多时间。

                          +

                          它这个要求我们修改kernel,使得每个进程都有一份自己的kernel page。至于要改什么,围绕着proc.c中,参照pagetable的生命周期摁改就行。还有一个地方它也提示了,就是要在swtch之前更换一下satp的值。

                          +

                          接下来,我说说我思考的几个点以及犯错的地方。

                          +
                          为什么要这么干

                          看完题目,我的第一印象是,这么干有啥用。。。因为我觉得以前那个所有进程共用内核页表确实很好了,没有必要每个进程配一个后来才发现,这个跟下面那个是连在一起的,目的是 allow the kernel to directly dereference user pointers.。所以,我们下面会把用户的pgtbl和这里dump出来的kpgtbl合在一起。

                          +

                          具体来说:

                          +

                          通常,进行地址翻译的时候,计算机硬件(即内存管理单元MMU)都会自动的查找对应的映射进行翻译(需要设置satp寄存器,将需要使用的页表的地址交给该寄存器)。

                          +

                          然而,在xv6内核需要翻译用户的虚拟地址时,因为内核页表不含对应的映射,计算机硬件不能自动帮助完成这件事。因此,我们需要先找到用户程序的页表,仿照硬件翻译的流程,一步一步的找到对应的物理地址,再对其进行访问。walkaddr】这也就会导致copyin之类需要涉及内核和用户态交互的函数效率低下。

                          +

                          为了解决这个问题,我们尝试将用户页表也囊括进内核页表映射来。但是,如果将所有进程的用户页表都合并到同一个内核全局页表是不现实的。因而,我们决定换一个角度,让每个进程都仅有一张内核态和用户态共用的页表,每次切换进程时切换页表,这样就构造出了个全局的假象。

                          +

                          这两次实验就是为了实现该任务。在本次实验中,我们首先先实现内核页表的分离。

                          +
                          关于myproc()

                          在allocproc中初始化的时候,我一开始是这么写的:

                          +
                          // in proc.c allocproc()
                          perproc_kvminit();
                          + +
                          // in vm.c
                          pagetable_t
                          perproc_kvminit()
                          {
                          struct proc* p = myproc();
                          p->kpgtbl = (pagetable_t) kalloc();
                          memset(p->kpgtbl, 0, PGSIZE);

                          // uart registers
                          pkvmmap(p->kpgtbl,UART0, UART0, PGSIZE, PTE_R | PTE_W);
                          // ...
                          return pt;
                          }
                          + +

                          这样会死得很惨,爆出如下panic:

                          +

                          image-20230114011100370

                          +

                          通过hints的调试贴士

                          -

                          MIT6.S081 lab8 locks

                          +

                          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.

                          -

                          代码

                          定义
                          struct {
                          struct spinlock kmem_locks[NCPU];
                          struct run *freelists[NCPU];
                          } kmem;
                          +

                          我发现程序在这里绷掉了:

                          +
                          p->kpgtbl = (pagetable_t) kalloc();
                          -
                          初始化

                          由于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;
                          }
                          }
                          }
                          +

                          而且显而易见,是系统启动时崩的。

                          +

                          经过了漫长的思考,我震惊地发现了它为什么崩了()

                          +

                          首先,这段代码语法上是没有问题的。它固然犯了发布未初始化完成的对象这样的并发错误【我有罪】,也破坏了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;
                          }
                          -
                          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();
                          }
                          +

                          我一路顺着os启动的路径找,也想不出来这能有什么错,因而非常迷茫。

                          +

                          此时我灵光一闪,会不会是myproc()在os刚启动的时候是发挥不了作用的?于是我一路顺着myproc的代码看下去:

                          +
                          struct proc*
                          myproc(void) {
                          push_off();
                          struct cpu *c = mycpu();
                          struct proc *p = c->proc;
                          pop_off();
                          return p;
                          }
                          -
                          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;
                          }
                          +

                          那么,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();
                          }
                          -

                          Buffer cache

                          buffer cache的结构其实跟kalloc的内存分配结构有一定的类似之处,都是采用链表管理,但是buffer cache的实现相较于kalloc更为复杂。

                          +

                          创建完进程后,就进入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;
                          // ...
                          + +

                          因而,c->proc是在创建进程的第一次调度后初始化的,也即,myproc只有在执行第一次scheduler之后才可以调用。而!!!

                          +

                          当执行调度前的userinit时:

                          +
                          void
                          userinit(void)
                          {
                          struct proc *p;

                          p = allocproc();
                          initproc = p;
                          + +

                          它进行了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();
                          }
                          + +

                          当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);
                          + +

                          但是这样还没有结束。因为我们除了得更换目前的页表,还得更换trapframe中的内核页表相关的东西:

                          +
                          struct trapframe {
                          /* 0 */ uint64 kernel_satp; // kernel page table
                          /* 8 */ uint64 kernel_sp; // top of process's kernel stack
                          }
                          + +

                          为啥还要更换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
                          + +

                          所以,为了以后系统调用能顺利自发进行,我们需要把栈帧也一起换掉。怎么换呢?我们是否还要在一些地方人工把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;
                          + +
                          // in vm.c
                          pagetable_t
                          perproc_kvminit()
                          {
                          pagetable_t pt = (pagetable_t) kalloc();
                          memset(pt, 0, PGSIZE);

                          // uart registers
                          pkvmmap(pt,UART0, UART0, PGSIZE, PTE_R | PTE_W);

                          // virtio mmio disk interface
                          pkvmmap(pt,VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);

                          // CLINT
                          pkvmmap(pt,CLINT, CLINT, 0x10000, PTE_R | PTE_W);

                          // PLIC
                          pkvmmap(pt,PLIC, PLIC, 0x400000, PTE_R | PTE_W);

                          // map kernel text executable and read-only.
                          pkvmmap(pt,KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);

                          // map kernel data and the physical RAM we'll make use of.
                          pkvmmap(pt,(uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);

                          // map the trampoline for trap entry/exit to
                          // the highest virtual address in the kernel.
                          pkvmmap(pt,TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
                          return pt;
                          }
                          + +
                          // in 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");
                          }
                          + +
                          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");
                          }
                          + +
                          修改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.proc.c freeproc()
                          if(p->kpgtbl)
                          proc_freekpgtbl(p->kpgtbl,p->kstack);
                          p->kpgtbl = 0;
                          + +
                          extern char etext[];  // kernel.ld sets this to end of kernel code.

                          void
                          proc_freekpgtbl(pagetable_t pagetable,uint64 stack )
                          {
                          uvmunmap(pagetable, UART0, 1, 0);
                          uvmunmap(pagetable, VIRTIO0, 1, 0);
                          uvmunmap(pagetable, CLINT, 0x10000/(uint64)PGSIZE, 0);
                          uvmunmap(pagetable, PLIC, 0X400000/(uint64)PGSIZE, 0);
                          uvmunmap(pagetable, KERNBASE, (uint64)((uint64)etext-KERNBASE)/PGSIZE, 0);
                          uvmunmap(pagetable, (uint64)etext,(PHYSTOP-(uint64)etext)/PGSIZE, 0);
                          //kvmmap(KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
                          uvmunmap(pagetable, TRAMPOLINE, 1, 0);
                          uvmunmap(pagetable, stack, 1,1 );
                          uvmfree(pagetable, 0);
                          }
                          + +

                          Simplify copyin/copyinstr

                          +

                          参考:

                          +

                          6.S081学习记录-lab3

                          +
                          -

                          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.

                          +

                          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.

                          -

                          感想

                          初见思路

                          我想我们可以这么实现:

                          -

                          首先有一个双向链表,接收着所有空闲无设备分配的buf。然后再有多个双向链表桶,以设备号为索引值。

                          -

                          设备号数量,也即hash table的大小定义在kernel/param.h中:

                          -
                          #define NDEV         10 
                          +
                          +

                          Replace the body of copyin in kernel/vm.c with a call to copyin_new (defined in kernel/vmcopyin.c); do the same for copyinstr and copyinstr_new. Add mappings for user addresses to each process’s kernel page table so that copyin_new and copyinstr_new work.

                          +
                          +

                          感想

                          这题很直观的思路是,在每个user pagetable添加映射的地方也添加kpgtbl的映射。但问题是,“每个user pagetable添加映射的地方”都是哪?

                          +
                          误入幻想

                          我一开始想着偷偷懒,直接在proc.c和vm.c中每个操纵pagetable的地方都加上对kpgtbl的操纵。但很快我就给搞晕了。这时候,我心中萌生一计【PS:下面说的最后都没成功】:我直接快进到把proc结构中的pagetable属性给删了,然后每个出现p->pagetable的地方,都用p->kpgtbl代替,直接让两表合为一表,然后之后make的时候哪里报错改哪里,这不就一劳永逸地把所有出现pagetable的地方都改为kpgtbl了嘛。我振奋地去试了一下,将所有地方出现的pagetable都替换成了kpgtbl,把proc.c中的proc_pagetable()proc_freepagetable()的出现的地方都换成了perproc_kvminit()以及proc_freekpgtbl(),还做了一个小细节,就是在userinit中调用的uvminit中,我把这样:

                          +
                          void
                          uvminit(pagetable_t pagetable, uchar *src, uint sz)
                          {
                          char *mem;

                          if(sz >= PGSIZE)
                          panic("inituvm: more than a page");
                          mem = kalloc();
                          memset(mem, 0, PGSIZE);
                          mappages(pagetable, 0, PGSIZE, (uint64)mem, PTE_W|PTE_R|PTE_X|PTE_U);
                          memmove(mem, src, sz);
                          }
                          -

                          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;
                          +

                          换成了这样:

                          +
                          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);
                          }
                          -
                          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;
                          }
                          }
                          +

                          最后,在启动的时候,卡在了初次调度切换不到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;
                          }
                          -
                          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]));
                          }
                          +
                          //in exec.c
                          // Commit to the user image.
                          oldpagetable = p->pagetable;
                          p->pagetable = pagetable;
                          -
                          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");
                          }
                          +
                          //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
                          -
                          正确思路

                          首先,大家似乎都是用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. +
                          9. 需要去掉freewalk中的panic

                            +

                            我的kvmcopy的实现是,user pagetable(下面简称up)和tp的相同虚拟地址共用同一页物理内存。也就是说,页表不一样,但所指向的物理内存是同一个。这样设计的目的是为了能够让tp及时用到up的更新后的数据。

                            +

                            这会导致啥呢?在进程释放时,需要一起调用proc_freepagetableproc_freekpgtblproc_freepagetable调用完后,所指向的那堆物理内存已经寄完了,如果再调用proc_freekpgtbl,显然,就会发生页表未释放但页表对应内存已经释放的问题,freewalk就会panic。因此,我简单粗暴地直接把freewalk的panic删掉了【抖】也许有别的解决方法,但我真是烦得不想想了放过我吧(

                            +
                          10. +
                          11. 好像暂时没有第二点了()

                            +
                          -

                          挺完美地符合了我的猜想。

                          -

                          【来自之后的学习:

                          -

                          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");
                          }
                          }
                          +
                          渔翁之利函数
                          // 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;
                          }
                          -

                          可知大概就是,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]));
                          }
                          }
                          +
                          修改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;
                          }
                          -

                          我在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");
                          }
                          +
                          // 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;
                          }
                          -

                          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的锁的情况。实在不知道怎么办了,想了很久还是没想出细粒度好方法(泪)总之代码先放在这里。

                          +
                          userinit
                          +

                          这一步不能忽视,因为内核启动的时候就需要用到copyinstr。

                          -
                          正确版本

                          请见我的github。

                          -
                          错误版本
                          static struct buf*
                          bget(uint dev, uint blockno)
                          {
                          // printf("bget\n");
                          uint hash = blockno % 13;

                          acquire(&(bcache.dev_locks[hash]));

                          // Is the block already cached?
                          for(struct buf* b = bcache.dev_heads[hash].next; b != &(bcache.dev_heads[hash]); b = b->next){
                          int initial_hold = holdingsleep(&b->lock);
                          release(&(bcache.dev_locks[hash]));

                          if (!initial_hold)
                          acquiresleep(&b->lock);

                          if(b->blockno == blockno&&b->dev == dev){ // 找到了
                          b->refcnt++;
                          b->timestamp = ticks;
                          // release(&(bcache.dev_locks[hash]));
                          // acquiresleep(&b->lock);
                          return b;
                          }

                          if (!initial_hold)
                          releasesleep(&b->lock);
                          acquire(&(bcache.dev_locks[hash]));
                          }

                          release(&(bcache.dev_locks[hash]));

                          // 没找到,进行LRU
                          // 遍历hash table,找到LRU,也即时间戳最小的且refcnt小于0的那一项

                          uint min_time = 4294967295;// uint的最大值。此处不能使用(uint)(-1)
                          struct buf* goal = 0;
                          for(int i = 0; i < NBUCKET; i++) {
                          uint time = 0;
                          acquire(&(bcache.dev_locks[i]));
                          for(struct buf* b = bcache.dev_heads[i].prev; b != &(bcache.dev_heads[i]); b = b->prev){
                          int initial_hold = holdingsleep(&b->lock);
                          release(&(bcache.dev_locks[i]));
                          if (!initial_hold)
                          acquiresleep(&b->lock);

                          if(b->refcnt == 0) {
                          time = b->timestamp;
                          if(time < min_time){
                          min_time = time;
                          if (goal) releasesleep(&goal->lock);
                          goal = b;
                          }
                          }
                          if (!initial_hold && goal != b) releasesleep(&b->lock);
                          acquire(&(bcache.dev_locks[i]));
                          }
                          release(&(bcache.dev_locks[i]));
                          }
                          // hashtable中存在着空闲buf
                          if(goal != 0){
                          // acquiresleep(&goal->lock);
                          goal->dev = dev;
                          goal->blockno = blockno;
                          goal->valid = 0;
                          goal->refcnt = 1;

                          // 将goal从其所在双向链表中移除
                          acquire(&(bcache.dev_locks[hash]));

                          goal->prev->next = goal->next;
                          goal->next->prev = goal->prev;

                          // 在新双向链表中添加goal
                          goal->prev = &(bcache.dev_heads[hash]);
                          goal->next = bcache.dev_heads[hash].next;

                          bcache.dev_heads[hash].next->prev = goal;
                          bcache.dev_heads[hash].next = goal;

                          release(&(bcache.dev_locks[hash]));

                          return goal;
                          }
                          panic("bget: no buffers");
                          }
                          - -
                          void
                          brelse(struct buf *b)
                          {
                          if(!holdingsleep(&b->lock))
                          panic("brelse");

                          uint hash = b->blockno%NBUCKET;

                          acquire(&(bcache.dev_locks[hash]));
                          b->refcnt--;
                          b->timestamp = ticks;
                          if (b->refcnt == 0) {
                          // no one is waiting for it.
                          b->next->prev = b->prev;
                          b->prev->next = b->next;

                          b->next = bcache.dev_heads[hash].next;
                          b->prev = &bcache.dev_heads[hash];
                          bcache.dev_heads[hash].next->prev = b;
                          bcache.dev_heads[hash].next = b;
                          }
                          release(&(bcache.dev_locks[hash]));
                          releasesleep(&b->lock);
                          }
                          +
                          // 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");
                          }
                          ]]> @@ -11038,53 +10740,354 @@ url访问填写http://localhost/webdemo4_war/*.do

                          Finally, kernel/pci.c contains code that searches for an E1000 card on the PCI bus when xv6 boots.

                          -

                          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.

                          +

                          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.

                          +
                          +

                          感想

                          说实话,一开始看题的时候真是感觉非常地哈人……但其实文档看着看着,心中也逐渐有了个大概,最后再结合下指导书的提示【当然不是后面那些保姆级的Hints】,最后写的也就八九不离十了。总体上来说,我觉得这次实验的代码还是很简单的,它主要难在探究过程,也就是从一开始什么也不懂,然后去阅读硬件设备的文档,结合代码尝试去理解,最后一步步写出来的过程。本次实验耗时六小时,我觉得肯定有不少于一半,甚至可能达到2/3的时间都耗费在理解上。这种从零开始探究的过程给了我很大的收获,同时也稍微提高了我面对挫折的能力。

                          +

                          这个实验确实设计得很有教育意义。除了我上面说的它锻炼了我的能力以外,它其实还具有比较深刻的工业意义。在看书的时候,书中这么写道:

                          +
                          +

                          In addition, the driver must understand the device’s hardware interface, which can be complex and poorly documented.

                          +
                          +

                          本次实验正是上述描述的简化版: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]。知道了这个之后,代码就简单了。

                            +
                            +

                            忏悔:我一开始真没反应过来。计网我记得是有一模一样的结构的,看来算是白做了2333

                            +
                            +
                          4. +
                          +

                          个人的推理过程

                          一开始就先懵懵懂懂地看指导书,直到看到这句话:

                          +
                          +

                          Browse the E1000 Software Developer’s Manual.

                          +
                          +

                          然后我这时连自己要干什么都迷迷糊糊,但姑且还是按他下面说的,准备先浏览第二章了。然而,我发现要我看我也还是看不懂啊,所以我就直接放弃了。【经验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都被标记,
                          // 就接收这个包,否则直接丢弃
                          + +

                          可以看到,跟正确思路虽然很多细节理解上有点问题,但是大体框架还是大差不差。然后再阅读指导书:

                          +
                          +

                          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没发成功。

                          +

                          等以后有精力再来看看吧。

                          +
                          +
                          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使用率极高

                          +
                          +

                          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前的指令执行。

                          +
                          +

                          这个就好明白多了。

                          +

                          这样一来,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指令分别用来禁用和启用中断。

                          +
                          +

                          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();
                          }
                          + +

                          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。为了避免死锁,文件系统代码总是按照前一句中提到的顺序获取锁。

                          +
                          +

                          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);
                          }
                          + +

                          可见只有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;
                          }
                          + +

                          我认为,这会导致死锁情况。假设计算机为多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);
                          + +
                          void
                          clockintr()
                          {
                          acquire(&tickslock);
                          ticks++;
                          wakeup(&ticks);
                          release(&tickslock);
                          }
                          + +

                          可以看到,这是非常典型的生产者消费者模型。生产者每生产一次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);
                          }
                          + +

                          有一点值得注意:

                          +
                          +

                          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在临界区结束才能释放的规范。

                          +

                          在查阅别人的说法的时候,我还看到了这个讨论:

                          +
                          +

                          中断中为什么不能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.

                          +
                          +

                          感想

                          总之,意思就是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。

                          +

                          第四步的时候我是一次窃取一页。我看到一个一次窃取多页的做法,我觉得很有想法,在这里附上链接:

                          +
                          +

                          MIT6.S081 lab8 locks

                          -

                          感想

                          说实话,一开始看题的时候真是感觉非常地哈人……但其实文档看着看着,心中也逐渐有了个大概,最后再结合下指导书的提示【当然不是后面那些保姆级的Hints】,最后写的也就八九不离十了。总体上来说,我觉得这次实验的代码还是很简单的,它主要难在探究过程,也就是从一开始什么也不懂,然后去阅读硬件设备的文档,结合代码尝试去理解,最后一步步写出来的过程。本次实验耗时六小时,我觉得肯定有不少于一半,甚至可能达到2/3的时间都耗费在理解上。这种从零开始探究的过程给了我很大的收获,同时也稍微提高了我面对挫折的能力。

                          -

                          这个实验确实设计得很有教育意义。除了我上面说的它锻炼了我的能力以外,它其实还具有比较深刻的工业意义。在看书的时候,书中这么写道:

                          +

                          代码

                          定义
                          struct {
                          struct spinlock kmem_locks[NCPU];
                          struct run *freelists[NCPU];
                          } kmem;
                          + +
                          初始化

                          由于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;
                          }
                          }
                          }
                          + +
                          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();
                          }
                          + +
                          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;
                          }
                          + +

                          Buffer cache

                          buffer cache的结构其实跟kalloc的内存分配结构有一定的类似之处,都是采用链表管理,但是buffer cache的实现相较于kalloc更为复杂。

                          -

                          In addition, the driver must understand the device’s hardware interface, which can be complex and poorly documented.

                          +

                          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.

                          -

                          本次实验正是上述描述的简化版:E1000的文档很详细,并且我们只用掌握一部分它的功能就行了。但虽然简化了,其探究过程的内在逻辑还是不会改变的。

                          -

                          总之,我很喜欢这次实验的设计。我的评价是牛逼。

                          -

                          思路

                          正确思路

                          Hints写得很详细,不做赘述了。主要就是明确一下数据结构的问题:

                          +

                          感想

                          初见思路

                          我想我们可以这么实现:

                          +

                          首先有一个双向链表,接收着所有空闲无设备分配的buf。然后再有多个双向链表桶,以设备号为索引值。

                          +

                          设备号数量,也即hash table的大小定义在kernel/param.h中:

                          +
                          #define NDEV         10 
                          + +

                          bget中,第一个循环仅需在设备链表中查找即可,第二个循环需要先看设备链表是否有空闲的对象,如果没有,则去接收所有空闲无设备分配的那个双向链表中窃取一个对象。

                          +

                          brelse中,则把要释放的buf对象添加在head中即可。

                          +

                          因而,我们要做以下几件事:

                            -
                          1. rx_ring和tx_ring是两个分开的队列

                            -

                            它们只是结构一模一样,都是阴影部分表示software持有,白色部分表示硬件持有。

                            -

                            因而,对于rx来说,白色部分表示需要传给协议栈的包,因而我们需要把白色部分转化为阴影部分;对于tx来说,白色部分表示网卡将要发送的包,因而我们需要把阴影部分转化为白色部分。

                            -

                            image-20230220234406239

                            +
                          2. 修改bcache的定义

                            +

                            添加数量为设备号的head数组,以及对应的锁

                          3. -
                          4. rx_mbufs和tx_mbufs

                            -

                            一开始不知道这俩是啥,后来才意识到,这俩和第1点的那俩其实是下标一一对应的关系。也就是说rx_ring[i]这个descriptor接收到的数据存在rx_mbufs[i],tx_ring[i]要发送的数据存在tx_mbufs[i]。知道了这个之后,代码就简单了。

                            -
                            -

                            忏悔:我一开始真没反应过来。计网我记得是有一模一样的结构的,看来算是白做了2333

                            -
                            +
                          5. 初始化bcache

                            +
                          6. +
                          7. 添加工具函数:将一个buf加入一个双向链表;从一个双向链表中得到一个buf

                            +
                          8. +
                          9. bgetbrelse

                          -

                          个人的推理过程

                          一开始就先懵懵懂懂地看指导书,直到看到这句话:

                          -
                          -

                          Browse the E1000 Software Developer’s Manual.

                          -
                          -

                          然后我这时连自己要干什么都迷迷糊糊,但姑且还是按他下面说的,准备先浏览第二章了。然而,我发现要我看我也还是看不懂啊,所以我就直接放弃了。【经验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都被标记,
                          // 就接收这个包,否则直接丢弃
                          +

                          看起来确实好像可以实现的样子,但是这个问题在于,这么做就直接破坏了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;
                          -

                          可以看到,跟正确思路虽然很多细节理解上有点问题,但是大体框架还是大差不差。然后再阅读指导书:

                          +
                          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;
                          }
                          }
                          + +
                          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]));
                          }
                          + +
                          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");
                          }
                          + +
                          正确思路

                          首先,大家似乎都是用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

                          -

                          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.

                          +

                          注:关于此处的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。】

                          -

                          就差不多是正确思路了。transmit的实现也是同理

                          -

                          代码

                          -

                          以下代码不知道为什么过不了test,我跟别人的逻辑一模一样也还是不行emmm

                          -

                          它的问题是,不会接收到外界的返ping,导致进程一直等待网卡IO,所以kerneltrap一直触发不了,无法正常网卡读写,从而导致fileread会一直处于sleep等待状态,整个系统就沉睡了【】我感觉应该是transmit没发成功。

                          -

                          等以后有精力再来看看吧。

                          +

                          我想来想去不知道这个错到底怎么爆的,看了下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");
                          }
                          }
                          + +

                          可知大概就是,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]));
                          }
                          }
                          + +

                          我在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的锁的情况。实在不知道怎么办了,想了很久还是没想出细粒度好方法(泪)总之代码先放在这里。

                          -
                          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);
                          }
                          +
                          正确版本

                          请见我的github。

                          +
                          错误版本
                          static struct buf*
                          bget(uint dev, uint blockno)
                          {
                          // printf("bget\n");
                          uint hash = blockno % 13;

                          acquire(&(bcache.dev_locks[hash]));

                          // Is the block already cached?
                          for(struct buf* b = bcache.dev_heads[hash].next; b != &(bcache.dev_heads[hash]); b = b->next){
                          int initial_hold = holdingsleep(&b->lock);
                          release(&(bcache.dev_locks[hash]));

                          if (!initial_hold)
                          acquiresleep(&b->lock);

                          if(b->blockno == blockno&&b->dev == dev){ // 找到了
                          b->refcnt++;
                          b->timestamp = ticks;
                          // release(&(bcache.dev_locks[hash]));
                          // acquiresleep(&b->lock);
                          return b;
                          }

                          if (!initial_hold)
                          releasesleep(&b->lock);
                          acquire(&(bcache.dev_locks[hash]));
                          }

                          release(&(bcache.dev_locks[hash]));

                          // 没找到,进行LRU
                          // 遍历hash table,找到LRU,也即时间戳最小的且refcnt小于0的那一项

                          uint min_time = 4294967295;// uint的最大值。此处不能使用(uint)(-1)
                          struct buf* goal = 0;
                          for(int i = 0; i < NBUCKET; i++) {
                          uint time = 0;
                          acquire(&(bcache.dev_locks[i]));
                          for(struct buf* b = bcache.dev_heads[i].prev; b != &(bcache.dev_heads[i]); b = b->prev){
                          int initial_hold = holdingsleep(&b->lock);
                          release(&(bcache.dev_locks[i]));
                          if (!initial_hold)
                          acquiresleep(&b->lock);

                          if(b->refcnt == 0) {
                          time = b->timestamp;
                          if(time < min_time){
                          min_time = time;
                          if (goal) releasesleep(&goal->lock);
                          goal = b;
                          }
                          }
                          if (!initial_hold && goal != b) releasesleep(&b->lock);
                          acquire(&(bcache.dev_locks[i]));
                          }
                          release(&(bcache.dev_locks[i]));
                          }
                          // hashtable中存在着空闲buf
                          if(goal != 0){
                          // acquiresleep(&goal->lock);
                          goal->dev = dev;
                          goal->blockno = blockno;
                          goal->valid = 0;
                          goal->refcnt = 1;

                          // 将goal从其所在双向链表中移除
                          acquire(&(bcache.dev_locks[hash]));

                          goal->prev->next = goal->next;
                          goal->next->prev = goal->prev;

                          // 在新双向链表中添加goal
                          goal->prev = &(bcache.dev_heads[hash]);
                          goal->next = bcache.dev_heads[hash].next;

                          bcache.dev_heads[hash].next->prev = goal;
                          bcache.dev_heads[hash].next = goal;

                          release(&(bcache.dev_locks[hash]));

                          return goal;
                          }
                          panic("bget: no buffers");
                          }
                          + +
                          void
                          brelse(struct buf *b)
                          {
                          if(!holdingsleep(&b->lock))
                          panic("brelse");

                          uint hash = b->blockno%NBUCKET;

                          acquire(&(bcache.dev_locks[hash]));
                          b->refcnt--;
                          b->timestamp = ticks;
                          if (b->refcnt == 0) {
                          // no one is waiting for it.
                          b->next->prev = b->prev;
                          b->prev->next = b->next;

                          b->next = bcache.dev_heads[hash].next;
                          b->prev = &bcache.dev_heads[hash];
                          bcache.dev_heads[hash].next->prev = b;
                          bcache.dev_heads[hash].next = b;
                          }
                          release(&(bcache.dev_locks[hash]));
                          releasesleep(&b->lock);
                          }
                          + ]]> @@ -12356,6 +12359,23 @@ url访问填写http://localhost/webdemo4_war/*.do
                          fork
                          for(int i=0;i<NFILEMAP;i++){
                          np->filemaps[i].isused = p->filemaps[i].isused;
                          np->filemaps[i].va = p->filemaps[i].va;
                          np->filemaps[i].okva = p->filemaps[i].okva;
                          np->filemaps[i].file = p->filemaps[i].file;
                          np->filemaps[i].length = p->filemaps[i].length;
                          np->filemaps[i].flags = p->filemaps[i].flags;
                          np->filemaps[i].offset = p->filemaps[i].offset;
                          np->filemaps[i].prot = p->filemaps[i].prot;
                          if(np->filemaps[i].file)
                          filedup(np->filemaps[i].file);
                          }

                          修改uvmcopy和uvmunmap

                          // in uvmunmap()
                          if((*pte & PTE_V) == 0){
                          *pte = 0;
                          continue;
                          }
                          // in uvmcopy()
                          if((*pte & PTE_V) == 0)
                          //panic("uvmcopy: page not present");
                          continue;
                          +]]> +
                          + + 各种配环境中遇到的问题 + /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/ + +
                        5. 记录一次vm扩容

                          +
                        6. +
                        7. 开发中遇到的链接小问题

                          +
                        8. +
                        9. rtt硬件环境搭建

                          +
                        10. +
                        11. 内核编译

                          +
                        12. +
                        13. 防火墙

                          +
                          sudo ufw status numbered # 查看
                          sudo ufw delete 数字 # 删除某条记录
                          # 开放IP地址XX.XX.XX.XX的22 tcp端口
                          sudo ufw allow from XX.XX.XX.XX to any port 22 proto tcp
                        14. +
                        ]]> @@ -12718,41 +12738,151 @@ url访问填写http://localhost/webdemo4_war/*.do
                      7. 在fs文件夹下创建文件proc_dev.c,编写proc文件的处理函数。代码如下:

                        #include <linux/fs.h>
                        #include <unistd.h>
                        #include <asm/segment.h>
                        #include <stdarg.h>
                        #include <linux/sched.h>
                        #include <sys/types.h>
                        #include <linux/kernel.h>

                        #define set_bit(nr,addr) ({\
                        register int res ; \
                        __asm__ __volatile__("btsl %2,%3\n\tsetb %%al": \
                        "=a" (res):"0" (0),"r" (nr),"m" (*(addr))); \
                        res;})

                        struct task_struct** p=&FIRST_TASK;
                        char s[100];
                        int flag;

                        extern int psinfo_handler(off_t* f_pos,char* buf);
                        extern int hdinfo_handler(off_t* f_pos,char* buf);

                        int proc_handler(unsigned short dev,char* buf,int count,off_t* f_pos){
                        //根据设备编号,把不同的内容写入到用户空间的 buf
                        switch(dev){
                        case 0:
                        return psinfo_handler(f_pos,buf);
                        case 1:
                        return hdinfo_handler(f_pos,buf);
                        default:
                        break;
                        }
                        return -1;
                        }

                        //在内核态和用户态间传递数据
                        int put_into_buf(char* buf,char* s){
                        int cnt=0;
                        while(s[cnt]!='\0'){
                        put_fs_byte(s[cnt++],buf++);
                        }
                        return cnt;
                        }

                        int sprintf(char* buf,const char* fmt,...){
                        va_list args;int i;
                        va_start(args,fmt);
                        i=vsprintf(buf,fmt,args);
                        va_end(args);
                        return i;
                        }

                        int psinfo_handler(off_t* f_pos,char* buf){
                        int i;
                        //初始化字符串
                        for(i=0;i<100;i++) s[i]=0;

                        //如果是第一次read,需要在屏幕上打印列表头,并且重置p指针为进程队列头
                        if((*f_pos)==0){
                        sprintf(s,"pid\tstate\tfather\tcounter\tstart_time\n");
                        p=&FIRST_TASK;
                        }
                        //到达文件末尾
                        if((*p)==NULL){
                        return 0;
                        }

                        //每次仅输出一行
                        if((*f_pos)!=0){
                        sprintf(s,"%ld\t%ld\t%ld\t%ld\t%ld\n",(*p)->pid,(*p)->state,(*p)->father,(*p)->counter,(*p)->start_time);
                        p++;
                        }

                        int cnt=put_into_buf(buf,s);
                        *f_pos+=cnt;

                        return cnt;
                        }

                        //可参考fs/super.c mount_root()
                        int hdinfo_handler(off_t* f_pos,char* buf){
                        //防止循环多次打印
                        if(flag==1){
                        flag=0;
                        return -1;
                        }
                        struct super_block* sb;
                        sb=get_super(0x301);/*磁盘设备号 3*256+1*/

                        int free=0;
                        int i=sb->s_nzones;
                        while (-- i >= 0)
                        if (!set_bit(i&8191,sb->s_zmap[i>>13]->b_data))
                        free++;
                        sprintf(s,"total_blocks:\t%d\nfree_blocks:\t%d\nused_blocks:\t%d\n",sb->s_nzones,free,sb->s_nzones-free);
                        int cnt=put_into_buf(buf,s);
                        flag=1;
                        return cnt;
                        }

                        -

                        运行结果:

                        - +

                        运行结果:

                        + + +

                        这部分踩过的坑:

                        1.LAST_TASK 的定义

                        +

                        对于LAST_TASK,我本来的理解是,当前所有进程的最后一个。

                        +

                        本来我设的是跟schedule一样,另p=LAST_TASK,从末尾开始打印。我那时其余代码跟上面一样,就只是把上面的FIRST改成LAST,结果输出为空,调试发现LAST_TASK==NULL。

                        +

                        然后打开sched.h,看到LAST_TASK的定义:

                        +
                        #define LAST_TASK task[NR_TASKS-1]
                        + +

                        原来它就是单纯简单粗暴地指“最后一个”进程23333

                        +

                        我们目前当前的进程数量远远小于进程的最大数量,因此最大数量编号的那个进程自然也就是空的了。

                        +

                        2.char s[100]={0};

                        +

                        用这个的时候编译报错:undefined reference to ’memset‘

                        +

                        说明这个简略写法其实本质是用的memset,而要用memset的话需要包含头文件string.h。经测试得包含了string.h后确实就好使了。

                        +
                        //s_imap_blocks、ns_zmap_blocks、

                        //total_blocks、free_blocks、used_blocks、total_inodes

                        for(i=0;is_zmap_blocks;i++)

                        {

                        bh=sb->s_zmap[i];

                        db=(char*)bh->b_data;

                        for(j=0;j<1024;j++){

                        for(k=1;k<=8;k++){

                        if((used_blocks+free_blocks)>=total_blocks)

                        break;

                        if( *(db+j) & k)
                        used_blocks++;

                        else

                        free_blocks++;

                        }
                        + +

                        3.我发现一件事

                        +

                        我第一次把init/main.c写错了,写成:

                        +
                        mkdir("/proc",0755);
                        mknod("/proc/psinfo",S_IFPROC|0400,0);
                        mknod("/proc/hdinfo",S_IFPROC|0400,0);
                        mknod("/proc/inodeinfo",S_IFPROC|0400,0);
                        + +

                        设别号忘了改了。然后进行了一次编译,运行。

                        +

                        之后我发现错了,就改成了

                        +
                        mkdir("/proc",0755);
                        mknod("/proc/psinfo",S_IFPROC|0400,0);
                        mknod("/proc/hdinfo",S_IFPROC|0400,1);
                        mknod("/proc/inodeinfo",S_IFPROC|0400,2);
                        + +

                        再次编译运行,结果上面的那个错还是没改回来

                        +

                        直到我手动把proc文件夹删了,再重新读一次磁盘加载proc文件夹,才回归正常。

                        +

                        感想

                        本次实验耗时:下午一点到晚上九点半()

                        +

                        本实验通过对proc虚拟文件的编写流程,实际上让我们体会到了“一切皆文件”的思想。

                        +

                        什么东西都可以是文件,只不过它们有不同的文件类型和不同的read/write处理函数。

                        +

                        对于终端设备和磁盘,其read/write函数本质上是在用out指令跟它的缓冲区交互,只不过磁盘比终端设备抽象层次更深,包含了文件系统的层层封装。

                        +

                        对于虚拟文件,其read/write函数本质上就是与内存交互,通过一段逻辑【处理函数】将内存存储的当前操作系统信息实时显示出来,而不需要存储。

                        +

                        还有,参考文章那篇的代码写的很好,快去看!

                        +]]> + + labs + + + + 对GRUB和initramfs的小探究 + /2023/06/17/%E5%AF%B9GRUB%E5%92%8Cinitramfs%E7%9A%84%E5%B0%8F%E6%8E%A2%E7%A9%B6/ + 竞赛时对操作系统启动过程产生了些疑问,于是问题导向地浅浅探究了下GRUB和initramfs相关机制,相关笔记先放在这里了。

                        +

                        内核启动流程

                        在传统的BIOS系统中,计算机具体的启动流程如下:

                        +
                          +
                        1. 电源启动:当计算机的电源打开时,电源供电给计算机的硬件设备。
                        2. +
                        3. BIOS自检:计算机的BIOS固件会自检硬件设备,包括RAM、处理器、硬盘等,以确保它们正常工作。
                        4. +
                        5. 引导设备选择:BIOS会根据预先定义的启动顺序(通常是硬盘、光驱、USB等)选择一个启动设备。
                        6. +
                        7. MBR(Master Boot Record)加载:如果选择的启动设备是硬盘,BIOS会加载该硬盘的MBR,其中包含了引导加载程序。
                        8. +
                        9. GRUB加载:MBR中的引导加载程序通常是GRUB(或其他引导加载程序)。GRUB会被加载到计算机的内存中,并开始执行。
                        10. +
                        11. GRUB菜单:GRUB会显示一个菜单,列出可供选择的操作系统或内核。
                        12. +
                        13. 操作系统加载:用户选择操作系统后,GRUB会加载相应的操作系统或内核,并将控制权交给它。
                        14. +
                        +

                        在本次内核编译配置过程中,最主要探究的是文件系统的装载过程,也即介于6-7之间的部分。

                        +

                        概述

                        文件系统在启动流程中的发展历程可以分为以下三个部分:

                        +
                          +
                        1. GRUB文件系统

                          +

                          由 GRUB 自身通过 BIOS 提供的服务加载

                          +
                        2. +
                        3. initramfs

                          +

                          由GRUB加载,用于挂载真正的文件系统

                          +
                        4. +
                        5. 真正的根文件系统

                          +
                        6. +
                        +

                        下面,将介绍1和2两个流程。

                        +

                        GRUB

                        +

                        GRUB(GNU GRand Unified Bootloader)是一种常用的引导加载程序,用于在计算机启动时加载操作系统。

                        +

                        GRUB的主要功能是在计算机启动时提供一个菜单,让用户选择要启动的操作系统或内核。它支持多个操作系统,包括各种版本的Linux、Windows、BSD等。通过GRUB,用户可以在多个操作系统之间轻松切换。

                        +

                        除了操作系统选择,GRUB还提供了一些高级功能,例如引导参数的设置、内存检测、系统恢复等。它还支持在启动过程中加载内核模块和初始化RAM磁盘映像(initrd或initramfs)。

                        +

                        GRUB具有高度可配置性,允许用户自定义引导菜单、设置默认启动项、编辑内核参数等。它还支持引导加载程序间的链式引导,可以引导其他引导加载程序,如Windows的NTLDR。

                        +
                        +

                        GRUB的基本作用流程为:

                        +
                          +
                        1. BIOS加载MBR,MBR加载GRUB,开始执行GRUB程序
                        2. +
                        3. GRUB程序会读取grub.cfg配置文件
                        4. +
                        5. GRUB程序依据配置文件,进行内核的加载、根文件系统的挂载等操作,最后将主导权转交给内核
                        6. +
                        +

                        grub.cfg

                        内核启动时,GRUB程序会读取/boot/grub/目录下的GRUB配置文件grub.cfg,其中记录了所有GRUB菜单可供选择的内核选项(menuentry)及其对应的启动依赖参数。以6.4.0内核选项为例:

                        +
                        # menuentry标识着GRUB菜单中的一个内核选项
                        menuentry 'Ubuntu' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-XXX' {
                        recordfail # 记录上次启动是否失败,用于处理启动失败的情况
                        load_video # 加载视频驱动模块,用于在启动过程中显示图形界面
                        gfxmode $linux_gfx_mode # 设置图形模式
                        insmod gzio # 加载gzio模块,提供对GZIP压缩和解压缩功能的支持
                        # 如果是在Xen虚拟化平台上,则加载xzio和lzopio模块
                        if [ x$grub_platform = xxen ]; then insmod xzio; insmod lzopio; fi

                        insmod part_gpt # 加载part_gpt模块,支持GUID分区表(GPT)
                        insmod ext2 # 加载ext2模块,支持ext2文件系统

                        # 设置文件系统的根分区
                        set root='hd0,gpt3'
                        if [ x$feature_platform_search_hint = xy ]; then
                        search --no-floppy --fs-uuid --set=root --hint-bios=hd0,gpt3 --hint-efi=hd0,gpt3 --hint-baremetal=ahci0,gpt3 XXX
                        else
                        search --no-floppy --fs-uuid --set=root XXX
                        fi

                        linux /boot/vmlinuz-6.4.0-rc3+ root=UUID=XXX ro text # 指定内核映像的路径和启动参数
                        initrd /boot/initrd.img-6.4.0-rc3+ # 指定initramfs映像的路径
                        }
                        + +

                        可以看到,grub.cfg主要记录了一些该内核启动需要的依赖module,以及内核映像和initramfs映像的路径

                        +

                        menuentry的代码中,有以下几个要点值得注意:

                        +
                          +
                        1. insmod gzio

                          +

                          由于加载gzio模块,提供对GZIP压缩和解压缩功能的支持。

                          +

                          看到这里我第一反应是觉得有点割裂,为啥这看着比较无关紧要的解压缩功能要在内核启动之前就需要有呢?于是我想起来在配置内核时,有一个选项是这样的:

                          +

                          image-20230616143835953

                          +

                          在配置选项中,我们选择了对initramfs的支持,并且勾选了Support initial ramdisk/ramfs compressed using gzip ,也即在编译时通过gzip压缩initramfs的大小以节省空间。

                          +

                          所以说,我们在内核启动之前,持有的initramfs处于被压缩的状态。故而,我们自然需要在内核启动之前安装gzio模块,从而支持之后对initramfs的解压缩了。

                          +
                        2. +
                        3. insmod ext2

                          +

                          这句代码说明,GRUB的临时文件系统为ext2类型,这句代码事实上是在安装GRUB建立临时文件的必要依赖包,从而GRUB程序之后才能建立其临时文件系统、从/boot/initrd.img获取initramfs映像。

                          +
                        4. +
                        5. linux /boot/vmlinuz-6.4.0-rc3+ root=UUID=XXX ro text

                          +

                          指定了启动参数,也即将根文件系统以只读(ro)的方式挂载在root=UUID=XXX对应的块设备上,并且默认以text方式(也即非图形化的Shell界面)启动内核。

                          +

                          此处的启动参数可在下一个部分介绍的grub文件中个性化。

                          +
                        6. +
                        +

                        grub.cfg的生成与修改

                        实际运用中,很多时候需要对启动参数进行一些修改。下面介绍两种修改grub.cfg的方法。

                        +

                        /etc/default/grub

                        可以看到,grub.cfg其实格式较为固定(也即由一系列内容也比较相似的menuentry构成)。因而,实际上我们是通过grub.d生成grub.cfg的(6.S081实验中事实上也涉及了这一点),而/etc/default/grub则是GRUB程序以及grub.cfg生成的配置文件。下面介绍下该文件主要有哪些配置选项。

                        +
                        # If you change this file, run 'update-grub' afterwards to update
                        # /boot/grub/grub.cfg.
                        # For full documentation of the options in this file, see:
                        # info -f grub -n 'Simple configuration'

                        # 开机时GRUB界面的持续时间,此处设置为30s
                        GRUB_TIMEOUT=30
                        GRUB_CMDLINE_LINUX=""

                        # 不使用图形化界面
                        #GRUB_TERMINAL=console
                        # 图形化界面的大小
                        #GRUB_GFXMODE=640x480
                        # 不使用UUID
                        #GRUB_DISABLE_LINUX_UUID=true

                        # 隐藏recovery mode
                        #GRUB_DISABLE_RECOVERY="true"
                        + +

                        重点看下这几个参数:

                        +
                          +
                        1. GRUB_CMDLINE_LINUX

                          +

                          表示最终生成的grub.cfg中的每一个menuentry中的linux那一行需要附加什么参数。

                          +

                          例如说,如果设置为:

                          +
                          # 表示initramfs在挂载真正的根文件系统之前,需要等待120s,用于防止磁盘没准备好导致的挂载失败
                          GRUB_CMDLINE_LINUX="rootdelay=120"
                          -

                          这部分踩过的坑:

                          1.LAST_TASK 的定义

                          -

                          对于LAST_TASK,我本来的理解是,当前所有进程的最后一个。

                          -

                          本来我设的是跟schedule一样,另p=LAST_TASK,从末尾开始打印。我那时其余代码跟上面一样,就只是把上面的FIRST改成LAST,结果输出为空,调试发现LAST_TASK==NULL。

                          -

                          然后打开sched.h,看到LAST_TASK的定义:

                          -
                          #define LAST_TASK task[NR_TASKS-1]
                          +

                          那么,最终在menuentry中的启动参数就为:

                          +
                          linux   /boot/vmlinuz-6.4.0-rc3+ root=UUID=XXX ro rootdelay=120 text
                          -

                          原来它就是单纯简单粗暴地指“最后一个”进程23333

                          -

                          我们目前当前的进程数量远远小于进程的最大数量,因此最大数量编号的那个进程自然也就是空的了。

                          -

                          2.char s[100]={0};

                          -

                          用这个的时候编译报错:undefined reference to ’memset‘

                          -

                          说明这个简略写法其实本质是用的memset,而要用memset的话需要包含头文件string.h。经测试得包含了string.h后确实就好使了。

                          -
                          //s_imap_blocks、ns_zmap_blocks、

                          //total_blocks、free_blocks、used_blocks、total_inodes

                          for(i=0;is_zmap_blocks;i++)

                          {

                          bh=sb->s_zmap[i];

                          db=(char*)bh->b_data;

                          for(j=0;j<1024;j++){

                          for(k=1;k<=8;k++){

                          if((used_blocks+free_blocks)>=total_blocks)

                          break;

                          if( *(db+j) & k)
                          used_blocks++;

                          else

                          free_blocks++;

                          }
                          +

                          其他一些常见的选项:

                          +
                          # 直接以路径来标识块设备而非使用UUID。此为old option,建议尽量使用UUID
                          GRUB_CMDLINE_LINUX="root=/dev/sda3"
                          # 标明init进程(启动后第一个进程)的具体路径。此处指明为`/bin/sh`
                          GRUB_CMDLINE_LINUX="init=/bin/sh"
                        2. +
                        3. GRUB_DEFAULT

                          +

                          参考 可以用来指定重启时的内核选项。如GRUB_DEFAULT="1> 0"表示选择第一个菜单界面的第2栏(Advanced for Ubuntu)和第二个菜单的第1个内核。

                          +
                        4. +
                        +

                        在修改完grub文件之后,我们需要执行sudo update-grub,来重新生成grub.cfg文件供下次启动使用。

                        +

                        在GRUB界面直接修改

                        image-20230616151055620

                        +

                        我们可以在GRUB界面选中所需内核,按下e键:

                        +

                        image-20230616151122738

                        +

                        然后就可以对启动参数进行修改,^X退出。

                        +

                        值得注意的是,此修改仅对本次启动有效。如果需要长期修改,建议还是通过第一种方法去修改。

                        +

                        initramfs

                        GRUB程序会通过initrd.img启动initramfs,从而进行真正的根文件系统挂载。

                        +
                        +

                        initrd.img是一个Linux系统中的初始化内存盘(initial RAM disk)的映像文件。它是一个压缩的文件系统映像,通常在引导过程中加载到内存中,并提供了一种临时的根文件系统,以便在正式的根文件系统(通常位于硬盘上)可用之前提供必要的功能和模块。

                        +
                        +

                        我们可以通过unmkinitramfs /boot/initrd.img-6.4.0-rc3+ /tmp/initrd/命令解压initrd,探究里面到底有什么玩意。

                        +
                        ├── bin -> usr/bin
                        ├── conf
                        ├── etc
                        ├── init
                        ├── lib -> usr/lib
                        ├── lib32 -> usr/lib32
                        ├── lib64 -> usr/lib64
                        ├── libx32 -> usr/libx32
                        ├── run
                        ├── sbin -> usr/sbin
                        ├── scripts
                        ├── usr
                        └── var
                        init
                        -

                        3.我发现一件事

                        -

                        我第一次把init/main.c写错了,写成:

                        -
                        mkdir("/proc",0755);
                        mknod("/proc/psinfo",S_IFPROC|0400,0);
                        mknod("/proc/hdinfo",S_IFPROC|0400,0);
                        mknod("/proc/inodeinfo",S_IFPROC|0400,0);
                        +

                        可以看到,这实际上就是一个小型的文件系统,也即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?}"
                        }
                        -

                        设别号忘了改了。然后进行了一次编译,运行。

                        -

                        之后我发现错了,就改成了

                        -
                        mkdir("/proc",0755);
                        mknod("/proc/psinfo",S_IFPROC|0400,0);
                        mknod("/proc/hdinfo",S_IFPROC|0400,1);
                        mknod("/proc/inodeinfo",S_IFPROC|0400,2);
                        +

                        由于研究这个是错误驱动(乐),因而我只主要看了下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}"
                        }
                        -

                        再次编译运行,结果上面的那个错还是没改回来

                        -

                        直到我手动把proc文件夹删了,再重新读一次磁盘加载proc文件夹,才回归正常。

                        -

                        感想

                        本次实验耗时:下午一点到晚上九点半()

                        -

                        本实验通过对proc虚拟文件的编写流程,实际上让我们体会到了“一切皆文件”的思想。

                        -

                        什么东西都可以是文件,只不过它们有不同的文件类型和不同的read/write处理函数。

                        -

                        对于终端设备和磁盘,其read/write函数本质上是在用out指令跟它的缓冲区交互,只不过磁盘比终端设备抽象层次更深,包含了文件系统的层层封装。

                        -

                        对于虚拟文件,其read/write函数本质上就是与内存交互,通过一段逻辑【处理函数】将内存存储的当前操作系统信息实时显示出来,而不需要存储。

                        -

                        还有,参考文章那篇的代码写的很好,快去看!

                        +

                        可以看到,这里如果进入错误状态,最终就是这样的效果2333:

                        +

                        image-20230616153420011

                        ]]>
                        - labs + os竞赛
                        @@ -12912,133 +13042,6 @@ url访问填写http://localhost/webdemo4_war/*.dobooks - - 各种配环境中遇到的问题 - /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/ - -
                      8. 记录一次vm扩容

                        -
                      9. -
                      10. 开发中遇到的链接小问题

                        -
                      11. -
                      12. rtt硬件环境搭建

                        -
                      13. -
                      14. 内核编译

                        -
                      15. -
                      16. 防火墙

                        -
                        sudo ufw status numbered # 查看
                        sudo ufw delete 数字 # 删除某条记录
                        # 开放IP地址XX.XX.XX.XX的22 tcp端口
                        sudo ufw allow from XX.XX.XX.XX to any port 22 proto tcp
                      17. -
                      -]]> - - - 对GRUB和initramfs的小探究 - /2023/06/17/%E5%AF%B9GRUB%E5%92%8Cinitramfs%E7%9A%84%E5%B0%8F%E6%8E%A2%E7%A9%B6/ - 竞赛时对操作系统启动过程产生了些疑问,于是问题导向地浅浅探究了下GRUB和initramfs相关机制,相关笔记先放在这里了。

                      -

                      内核启动流程

                      在传统的BIOS系统中,计算机具体的启动流程如下:

                      -
                        -
                      1. 电源启动:当计算机的电源打开时,电源供电给计算机的硬件设备。
                      2. -
                      3. BIOS自检:计算机的BIOS固件会自检硬件设备,包括RAM、处理器、硬盘等,以确保它们正常工作。
                      4. -
                      5. 引导设备选择:BIOS会根据预先定义的启动顺序(通常是硬盘、光驱、USB等)选择一个启动设备。
                      6. -
                      7. MBR(Master Boot Record)加载:如果选择的启动设备是硬盘,BIOS会加载该硬盘的MBR,其中包含了引导加载程序。
                      8. -
                      9. GRUB加载:MBR中的引导加载程序通常是GRUB(或其他引导加载程序)。GRUB会被加载到计算机的内存中,并开始执行。
                      10. -
                      11. GRUB菜单:GRUB会显示一个菜单,列出可供选择的操作系统或内核。
                      12. -
                      13. 操作系统加载:用户选择操作系统后,GRUB会加载相应的操作系统或内核,并将控制权交给它。
                      14. -
                      -

                      在本次内核编译配置过程中,最主要探究的是文件系统的装载过程,也即介于6-7之间的部分。

                      -

                      概述

                      文件系统在启动流程中的发展历程可以分为以下三个部分:

                      -
                        -
                      1. GRUB文件系统

                        -

                        由 GRUB 自身通过 BIOS 提供的服务加载

                        -
                      2. -
                      3. initramfs

                        -

                        由GRUB加载,用于挂载真正的文件系统

                        -
                      4. -
                      5. 真正的根文件系统

                        -
                      6. -
                      -

                      下面,将介绍1和2两个流程。

                      -

                      GRUB

                      -

                      GRUB(GNU GRand Unified Bootloader)是一种常用的引导加载程序,用于在计算机启动时加载操作系统。

                      -

                      GRUB的主要功能是在计算机启动时提供一个菜单,让用户选择要启动的操作系统或内核。它支持多个操作系统,包括各种版本的Linux、Windows、BSD等。通过GRUB,用户可以在多个操作系统之间轻松切换。

                      -

                      除了操作系统选择,GRUB还提供了一些高级功能,例如引导参数的设置、内存检测、系统恢复等。它还支持在启动过程中加载内核模块和初始化RAM磁盘映像(initrd或initramfs)。

                      -

                      GRUB具有高度可配置性,允许用户自定义引导菜单、设置默认启动项、编辑内核参数等。它还支持引导加载程序间的链式引导,可以引导其他引导加载程序,如Windows的NTLDR。

                      -
                      -

                      GRUB的基本作用流程为:

                      -
                        -
                      1. BIOS加载MBR,MBR加载GRUB,开始执行GRUB程序
                      2. -
                      3. GRUB程序会读取grub.cfg配置文件
                      4. -
                      5. GRUB程序依据配置文件,进行内核的加载、根文件系统的挂载等操作,最后将主导权转交给内核
                      6. -
                      -

                      grub.cfg

                      内核启动时,GRUB程序会读取/boot/grub/目录下的GRUB配置文件grub.cfg,其中记录了所有GRUB菜单可供选择的内核选项(menuentry)及其对应的启动依赖参数。以6.4.0内核选项为例:

                      -
                      # menuentry标识着GRUB菜单中的一个内核选项
                      menuentry 'Ubuntu' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-XXX' {
                      recordfail # 记录上次启动是否失败,用于处理启动失败的情况
                      load_video # 加载视频驱动模块,用于在启动过程中显示图形界面
                      gfxmode $linux_gfx_mode # 设置图形模式
                      insmod gzio # 加载gzio模块,提供对GZIP压缩和解压缩功能的支持
                      # 如果是在Xen虚拟化平台上,则加载xzio和lzopio模块
                      if [ x$grub_platform = xxen ]; then insmod xzio; insmod lzopio; fi

                      insmod part_gpt # 加载part_gpt模块,支持GUID分区表(GPT)
                      insmod ext2 # 加载ext2模块,支持ext2文件系统

                      # 设置文件系统的根分区
                      set root='hd0,gpt3'
                      if [ x$feature_platform_search_hint = xy ]; then
                      search --no-floppy --fs-uuid --set=root --hint-bios=hd0,gpt3 --hint-efi=hd0,gpt3 --hint-baremetal=ahci0,gpt3 XXX
                      else
                      search --no-floppy --fs-uuid --set=root XXX
                      fi

                      linux /boot/vmlinuz-6.4.0-rc3+ root=UUID=XXX ro text # 指定内核映像的路径和启动参数
                      initrd /boot/initrd.img-6.4.0-rc3+ # 指定initramfs映像的路径
                      }
                      - -

                      可以看到,grub.cfg主要记录了一些该内核启动需要的依赖module,以及内核映像和initramfs映像的路径

                      -

                      menuentry的代码中,有以下几个要点值得注意:

                      -
                        -
                      1. insmod gzio

                        -

                        由于加载gzio模块,提供对GZIP压缩和解压缩功能的支持。

                        -

                        看到这里我第一反应是觉得有点割裂,为啥这看着比较无关紧要的解压缩功能要在内核启动之前就需要有呢?于是我想起来在配置内核时,有一个选项是这样的:

                        -

                        image-20230616143835953

                        -

                        在配置选项中,我们选择了对initramfs的支持,并且勾选了Support initial ramdisk/ramfs compressed using gzip ,也即在编译时通过gzip压缩initramfs的大小以节省空间。

                        -

                        所以说,我们在内核启动之前,持有的initramfs处于被压缩的状态。故而,我们自然需要在内核启动之前安装gzio模块,从而支持之后对initramfs的解压缩了。

                        -
                      2. -
                      3. insmod ext2

                        -

                        这句代码说明,GRUB的临时文件系统为ext2类型,这句代码事实上是在安装GRUB建立临时文件的必要依赖包,从而GRUB程序之后才能建立其临时文件系统、从/boot/initrd.img获取initramfs映像。

                        -
                      4. -
                      5. linux /boot/vmlinuz-6.4.0-rc3+ root=UUID=XXX ro text

                        -

                        指定了启动参数,也即将根文件系统以只读(ro)的方式挂载在root=UUID=XXX对应的块设备上,并且默认以text方式(也即非图形化的Shell界面)启动内核。

                        -

                        此处的启动参数可在下一个部分介绍的grub文件中个性化。

                        -
                      6. -
                      -

                      grub.cfg的生成与修改

                      实际运用中,很多时候需要对启动参数进行一些修改。下面介绍两种修改grub.cfg的方法。

                      -

                      /etc/default/grub

                      可以看到,grub.cfg其实格式较为固定(也即由一系列内容也比较相似的menuentry构成)。因而,实际上我们是通过grub.d生成grub.cfg的(6.S081实验中事实上也涉及了这一点),而/etc/default/grub则是GRUB程序以及grub.cfg生成的配置文件。下面介绍下该文件主要有哪些配置选项。

                      -
                      # If you change this file, run 'update-grub' afterwards to update
                      # /boot/grub/grub.cfg.
                      # For full documentation of the options in this file, see:
                      # info -f grub -n 'Simple configuration'

                      # 开机时GRUB界面的持续时间,此处设置为30s
                      GRUB_TIMEOUT=30
                      GRUB_CMDLINE_LINUX=""

                      # 不使用图形化界面
                      #GRUB_TERMINAL=console
                      # 图形化界面的大小
                      #GRUB_GFXMODE=640x480
                      # 不使用UUID
                      #GRUB_DISABLE_LINUX_UUID=true

                      # 隐藏recovery mode
                      #GRUB_DISABLE_RECOVERY="true"
                      - -

                      重点看下这几个参数:

                      -
                        -
                      1. GRUB_CMDLINE_LINUX

                        -

                        表示最终生成的grub.cfg中的每一个menuentry中的linux那一行需要附加什么参数。

                        -

                        例如说,如果设置为:

                        -
                        # 表示initramfs在挂载真正的根文件系统之前,需要等待120s,用于防止磁盘没准备好导致的挂载失败
                        GRUB_CMDLINE_LINUX="rootdelay=120"
                        - -

                        那么,最终在menuentry中的启动参数就为:

                        -
                        linux   /boot/vmlinuz-6.4.0-rc3+ root=UUID=XXX ro rootdelay=120 text
                        - -

                        其他一些常见的选项:

                        -
                        # 直接以路径来标识块设备而非使用UUID。此为old option,建议尽量使用UUID
                        GRUB_CMDLINE_LINUX="root=/dev/sda3"
                        # 标明init进程(启动后第一个进程)的具体路径。此处指明为`/bin/sh`
                        GRUB_CMDLINE_LINUX="init=/bin/sh"
                      2. -
                      3. GRUB_DEFAULT

                        -

                        参考 可以用来指定重启时的内核选项。如GRUB_DEFAULT="1> 0"表示选择第一个菜单界面的第2栏(Advanced for Ubuntu)和第二个菜单的第1个内核。

                        -
                      4. -
                      -

                      在修改完grub文件之后,我们需要执行sudo update-grub,来重新生成grub.cfg文件供下次启动使用。

                      -

                      在GRUB界面直接修改

                      image-20230616151055620

                      -

                      我们可以在GRUB界面选中所需内核,按下e键:

                      -

                      image-20230616151122738

                      -

                      然后就可以对启动参数进行修改,^X退出。

                      -

                      值得注意的是,此修改仅对本次启动有效。如果需要长期修改,建议还是通过第一种方法去修改。

                      -

                      initramfs

                      GRUB程序会通过initrd.img启动initramfs,从而进行真正的根文件系统挂载。

                      -
                      -

                      initrd.img是一个Linux系统中的初始化内存盘(initial RAM disk)的映像文件。它是一个压缩的文件系统映像,通常在引导过程中加载到内存中,并提供了一种临时的根文件系统,以便在正式的根文件系统(通常位于硬盘上)可用之前提供必要的功能和模块。

                      -
                      -

                      我们可以通过unmkinitramfs /boot/initrd.img-6.4.0-rc3+ /tmp/initrd/命令解压initrd,探究里面到底有什么玩意。

                      -
                      ├── bin -> usr/bin
                      ├── conf
                      ├── etc
                      ├── init
                      ├── lib -> usr/lib
                      ├── lib32 -> usr/lib32
                      ├── lib64 -> usr/lib64
                      ├── libx32 -> usr/libx32
                      ├── run
                      ├── sbin -> usr/sbin
                      ├── scripts
                      ├── usr
                      └── var
                      init
                      - -

                      可以看到,这实际上就是一个小型的文件系统,也即initramfs。它有自己的built-in Shell(BusyBox):

                      -

                      image-20230616151938951

                      -

                      有一些较少的Shell命令(bin和sbin目录下),以及用来挂载真正的根文件系统的代码逻辑(存储在scripts目录下)。【我猜】在正常情况下,系统会执行scripts下的脚本代码挂载真正的文件系统。当挂载出现异常时,系统就会将控制权交给initramfs内置的Shell BusyBox,由用户自己探究出了什么问题。

                      -

                      我们接下来可以追踪下initramfs的script目录下的文件系统挂载流程。

                      -

                      挂载真正文件系统的主要函数为local_mount_root

                      -
                      # 仅展示主要流程代码
                      local_mount_root()
                      {
                      # 预处理,获取参数等(也即上面grub.cfg配置的root=UUID)
                      local_top
                      if [ -z "${ROOT}" ]; then
                      panic "No root device specified. Boot arguments must include a root= parameter."
                      fi

                      # 根据UUID获取对应的块设备
                      local_device_setup "${ROOT}" "root file system"
                      ROOT="${DEV}"

                      # 挂载前的预处理
                      local_premount

                      # 挂载
                      mount ${roflag} ${FSTYPE:+-t "${FSTYPE}"} ${ROOTFLAGS} "${ROOT}" "${rootmnt?}"
                      }
                      - -

                      由于研究这个是错误驱动(乐),因而我只主要看了下local_device_setup

                      -
                      # $1=device ID to mount设备ID
                      # $2=optionname (for root and etc)要挂载的是什么玩意,此处应为root file system
                      # $3=panic if device is missing (true or false, default: true)
                      # Sets $DEV to the resolved device node $DEV是最终获取到的块设备
                      local_device_setup()
                      {
                      local dev_id="$1"
                      local name="$2"
                      local may_panic="${3:-true}"
                      local real_dev
                      local time_elapsed
                      local count

                      # 获取grub.cfg的rootdelay参数的设备等待时间。如果没有该参数,默认是30秒
                      local slumber=30
                      if [ "${ROOTDELAY:-0}" -gt $slumber ]; then
                      slumber=$ROOTDELAY
                      fi

                      # 等待设备
                      case "$dev_id" in
                      UUID=*|LABEL=*|PARTUUID=*|/dev/*)
                      FSTYPE=$( wait-for-root "$dev_id" "$slumber" )
                      ;;
                      *)
                      wait_for_udev 10
                      ;;
                      esac

                      # 等待结束了。如果条件为真,说明还是获取不到对应的设备,那就只能说明这个设备死了
                      # 所以我们就得把问题告诉用户,让用户自己解决,并且进入BusyBox Shell
                      # We've given up, but we'll let the user fix matters if they can
                      while ! real_dev=$(resolve_device "${dev_id}") ||
                      ! get_fstype "${real_dev}" >/dev/null; do
                      if ! $may_panic; then
                      echo "Gave up waiting for ${name}"
                      return 1
                      fi
                      echo "Gave up waiting for ${name} device. Common problems:"
                      echo " - Boot args (cat /proc/cmdline)"
                      echo " - Check rootdelay= (did the system wait long enough?)"
                      if [ "${name}" = root ]; then
                      echo " - Check root= (did the system wait for the right device?)"
                      fi
                      echo " - Missing modules (cat /proc/modules; ls /dev)"
                      panic "ALERT! ${dev_id} does not exist. Dropping to a shell!"
                      done

                      DEV="${real_dev}"
                      }
                      - -

                      可以看到,这里如果进入错误状态,最终就是这样的效果2333:

                      -

                      image-20230616153420011

                      -]]>
                      - - os竞赛 - -
                      状态机 /2023/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/ @@ -13079,8 +13082,140 @@ url访问填写http://localhost/webdemo4_war/*.do
                    6. 所以,我们可以出此暴论:在课程范围内,首先以moore的思想来设计状态机。如果该状态机可以被化简,那么这道题就要用mealy型的来做;如果不能,那么这道题就是得用moore型状态机来做。

                      一开始的那个时序锁的moore状态机不能化简,因此它是moore型。

                      -

                      这个点本来可以讲得更清楚一些的……只教会我们做题的套路有啥意思呢←_←

                      +

                      这个点本来可以讲得更清楚一些的……只教会我们做题的套路有啥意思呢←_←

                      +
                      +]]> + + + 网络是怎样连接的 + /2023/10/06/%E7%BD%91%E7%BB%9C%E6%98%AF%E6%80%8E%E6%A0%B7%E8%BF%9E%E6%8E%A5%E7%9A%84/ + +

                      此为《信息存储与管理(第二版):数字信息的存储、管理和保护》的看书总结,相当于是对存储技术的一个简单的名词入门。

                      + +

                      浏览器生成消息

                      本章节我印象最深的还是以前就不大了解的DNS,今天看到书的描写真有种豁然开朗的感觉。

                      +

                      DNS服务器用于保存域名—IP地址的映射对,为了增加查找效率,DNS根据域名的分级采用树形组织,例如hitsz.edu.cn/可以相当于是/cn/edu/hitsz,包含了/cnedu这几个域。根DNS服务器存储着根域,记录了所有一级域名对应DNS服务器的IP地址。所有的DNS服务器都会保存根服务器的IP地址。

                      +
                      +

                      世界上只有13个根DNS服务器IP地址,但是有很多台根DNS服务器。

                      +
                      +

                      主机需要手动配置DNS服务器地址。

                      +

                      当浏览器需要填写请求头时,它需要通过系统调用向操作系统发送DNS查询请求。操作系统将DNS请求发送给配置在主机上的DNS服务器(下称A),A再向根DNS服务器发送请求。根DNS服务器解析域名,返回下一级DNS服务器的IP地址。A再向下级DNS服务器再次发送请求,下级再返回下下级IP地址。以此类推,最终A就能得到目标IP地址的正确响应。整个过程如下图所示:

                      +

                      image-20231010132718116

                      +

                      与此同时,各个DNS服务器都会有定时刷新的缓存,从而加速了查找效率。

                      +

                      用电信号传输TCP/IP数据

                      TCP/IP

                      本章前面大多讨论TCP/IP具体协议内容,以前已经了解过很多次了就不多赘述。所以TCP/IP部分就以分点的形式随意列举一下:

                      +
                        +
                      1. IP 中还包括 ICMPA 协议和 ARPB 协议。ICMP 用于告知网络包传送过程中产生的错误以及各种控制消息,ARP 用于根据 IP 地址查询相应的以太网 MAC 地址。

                        +
                      2. +
                      3. 套接字中记录了用于控制通信操作的各种控制信息,协议栈则需要根据这些信息判断下一步的行动,【包括应用程序信息和协议栈状态信息】这就是套接字的作用。所以需要针对不同协议栈实现不同的socket。

                        +
                      4. +
                      5. 是的,回想当初CS144,也是socket来负责有特定消息时调用TCP相关函数来通知处理。

                        +
                      6. +
                      7. 连接 connect

                        +

                        连接实际上是通信双方交换控制信息,在套接字中记录这些必要信息并准备数据收发的一连串操作。

                        +
                          +
                        1. 应用程序向协议栈传ip地址
                        2. +
                        3. 本机向服务器发通信请求
                        4. +
                        5. 过程中分配通信缓冲区
                        6. +
                        +
                      8. +
                      9. 动态调整等待时间

                        +

                        image-20231012113725655

                        +
                      10. +
                      +

                      以太网

                        +
                      1. 以太网的定义

                        +

                        image-20231012113840636

                        +
                      2. +
                      3. 系统初始化时MAC地址的设置

                        +

                        MAC地址是上电后由驱动程序从ROM中读取的,而非自动获取的

                        +
                      4. +
                      5. 电信号转换【这个帅得不行】

                        +

                        为了区分连续的1或0,我们就需要同时发送数据信号和时钟信号,然而这样开销太大,因而我们引入了上升沿

                        +

                        上升沿本质上是数据信号和时钟信号叠加而成的结果,叠加方式是异或

                        +

                        image-20231012114006378

                        +

                        提到异或是否感觉豁然开朗?是的,这东西恢复时也是使用了异或的性质:接收方从帧头获取时钟频率从而得到时钟信号,跟收到的叠加信号进行再次叠加(异或),就可以获得原来的数据信号了。

                        +

                        我只能说牛逼,一直以来对异或的视角还停留在单纯的数字,这个波形的物理概念真的惊到我了。

                        +

                        实例:

                        +

                        image-20231012114350599

                        +
                      6. +
                      7. 半双工模式【同一时刻只能进行发or收】使用集线器,全双工模式【发or收可以并行】使用交换机。半双工模式需要进行载波监听碰撞检测。

                        +
                      8. +
                      9. 服务器的操作系统具备和路由器相同的包转发功能,当打开这一功能时,它就可以像路由器一样对包进行转发。在这种情况下,当收到不是发给自己的包的时候,就会像路由器一样执行包转发操作。

                        +
                      10. +
                      +]]>
                      + + books + +
                      + + 记录一次vm扩容 + /2023/09/27/%E8%AE%B0%E5%BD%95%E4%B8%80%E6%AC%A1vm%E6%89%A9%E5%AE%B9/ + 扩容

                      容量寄,但是df -h发现这次好像是不大一样的:

                      +

                      image-20230927191332920

                      +

                      查了一下,原来这是逻辑卷管理(LVM,Logical Volume Manger)。

                      +
                      +

                      参考

                      +

                      实现将多个硬盘和硬盘分区做成一个逻辑卷,并将逻辑卷统一管理。创建LVM顺序为:物理卷PV->卷组VG->逻辑卷LV。
                      物理卷(PV,Physical Volume):物理硬盘或分区;
                      卷组(VG,Volume Group):一个或多个物理卷集合;
                      逻辑卷(Logical Volume):供系统使用的元设备,虚拟分区。

                      +

                      LVM常用的命令:

                      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                      功能PV物理卷管理VG卷组管理LV逻辑卷管理
                      扫描pvscanvgscanlvscan
                      创建pvcreatevgcreatelvcreate
                      查看pvdisplayvgdisplaylvdisplay
                      删除pvremovevgremovelvremove
                      扩展/vgextendlvextend
                      缩容/vgreducelvreduce
                      +

                      接下来简要介绍其扩容步骤。

                      +
                        +
                      1. 在vmware中扩展磁盘容量

                        +

                        image-20230927191636392

                        +
                      2. +
                      3. sudo fdisk /dev/sda,进行磁盘分区

                        +

                        在fdisk中输入n,新建sda4分区,然后w保存。

                        +
                      4. +
                      5. 执行下列命令:

                        +
                        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 # 验证成功
                      6. +
                      +

                      一开始按照的这个,然后被坑惨了(悲)把lvm sig给抹了,导致之后resize2fs的时候报错,然后之后又不小心重启了,最后的最后只能重装。。。又是一晚上配环境。。。。

                      ]]>
                      @@ -13173,372 +13308,482 @@ url访问填写http://localhost/webdemo4_war/*.do。 ]]> - 网络是怎样连接的 - /2023/10/06/%E7%BD%91%E7%BB%9C%E6%98%AF%E6%80%8E%E6%A0%B7%E8%BF%9E%E6%8E%A5%E7%9A%84/ + 阅读JDK容器部分源码的心得体会1【Collection部分】 + /2022/10/16/%E9%98%85%E8%AF%BBJDK%E5%AE%B9%E5%99%A8%E9%83%A8%E5%88%86%E6%BA%90%E7%A0%81%E7%9A%84%E5%BF%83%E5%BE%97%E4%BD%93%E4%BC%9A1%E3%80%90Collection%E9%83%A8%E5%88%86%E3%80%91/ -

                      此为《信息存储与管理(第二版):数字信息的存储、管理和保护》的看书总结,相当于是对存储技术的一个简单的名词入门。

                      +

                      idea 替换注释正则表达式/\*{1,2}[\s\S]*?\*/

                      +

                      typora 替换图片asset

                      +

                      \!\[.*\]\(D:\\aWorkStorage\\hexo\\blog\\source\\_posts\\阅读JDK容器部分源码的心得体会1【Collection部分】\\(.*)\.png\)

                      +

                      替换结果{% asset_img $1.png %}

                      -

                      浏览器生成消息

                      本章节我印象最深的还是以前就不大了解的DNS,今天看到书的描写真有种豁然开朗的感觉。

                      -

                      DNS服务器用于保存域名—IP地址的映射对,为了增加查找效率,DNS根据域名的分级采用树形组织,例如hitsz.edu.cn/可以相当于是/cn/edu/hitsz,包含了/cnedu这几个域。根DNS服务器存储着根域,记录了所有一级域名对应DNS服务器的IP地址。所有的DNS服务器都会保存根服务器的IP地址。

                      + + + + +

                      迭代器相关接口

                      Iterable(I)

                      /*实现这个接口的类可用于for-each循环*/

                      public interface Iterable<T> {

                      Iterator<T> iterator();

                      /*对Iterator内每个元素实施此操作,直到遍历完或者抛出异常。*/
                      default void forEach(Consumer<? super T> action) {
                      Objects.requireNonNull(action);
                      for (T t : this) {
                      action.accept(t);
                      }
                      }

                      default Spliterator<T> spliterator() {
                      return Spliterators.spliteratorUnknownSize(iterator(), 0);
                      }
                      }
                      + +
                      +

                      引入迭代器的目的是为了**统一**“对容器里的元素进行遍历”这一操作。

                      +
                      +

                      Iterator(I)

                      public interface Iterator<E> {
                      //之后还有没有元素
                      boolean hasNext();

                      //返回当前所指元素,并且将iterator指针下移
                      E next();

                      //This call can be made only if neither remove nor add have been called after the last call to next or previous.
                      default void remove() {
                      throw new UnsupportedOperationException("remove");
                      }

                      default void forEachRemaining(Consumer<? super E> action) {
                      Objects.requireNonNull(action);
                      while (hasNext())
                      action.accept(next());
                      }
                      }
                      + +
                      +

                      注意,iterator并不指向具体元素,它指向的是元素的间隙

                      +

                      这些^就是iterator所指的位置。这样就能理解iterator的next和previous了吧2333

                      + + +

                      而set()所修改的元素,是其上一次调用next()或者previous()方法所返回的元素。

                      +

                      What’s the meaning of this source code of interface Collection in JAVA?

                      +
                      +

                      ListIterator(I)

                      public interface ListIterator<E> extends Iterator<E> {
                      boolean hasNext();
                      E next();
                      void remove();

                      //newly added as iterator below:
                      boolean hasPrevious();

                      E previous();

                      //返回后续调用 next 将返回的元素的索引。[应该就是当前元素索引]
                      int nextIndex();

                      int previousIndex();

                      //This call can be made only if neither remove nor add have been called after the last call to next or previous.
                      void set(E e);

                      //The element is inserted immediately before the element that would be returned by next, if any, and after the element that would be returned by previous, if any.
                      void add(E e);
                      }
                      + +
                      +

                      Note that the remove and set(Object) methods are not defined in terms of the cursor position; they are defined to operate on the last element returned by a call to next or previous().

                      +
                      +

                      Collection

                      Collection(I)

                      代码

                      /*
                      无直接实现类,一般用于需要使用多态传递参数的场合
                      其所有子类都必须有两个构造器: a void (no arguments) constructor, which creates an empty collection, and a constructor with a single argument of type Collection, which creates a new collection with the same elements as its argument. 这点语法上不会强制实现(因为接口不能强制构造方法),但其实是约定成俗的。

                      */
                      public interface Collection<E> extends Iterable<E> {

                      int size();

                      boolean isEmpty();

                      boolean contains(Object o);

                      Iterator<E> iterator();

                      //有关“safe”的问题讨论见下
                      Object[] toArray();

                      /*
                      Like the toArray() method, this method acts as bridge between array-based and
                      collection-based APIs. Further, this method allows precise control over the runtime
                      type of the output array, and may, under certain circumstances, be used to save
                      allocation costs.

                      Suppose x is a collection known to contain only strings. The following code can be
                      used to dump the collection into a newly allocated array of String:
                      String[] y = x.toArray(new String[0]);

                      说明还是有类型限制的
                      ArrayStoreException – if the runtime type of the specified array is not a supertype
                      of the runtime type of every element in this collection
                      */
                      <T> T[] toArray(T[] a);

                      /*
                      给集合增加一个元素。
                      If a collection refuses to add a particular element for any reason other than that it
                      already contains the element, it must throw an exception (rather than returning
                      false).
                      */
                      boolean add(E e);

                      boolean remove(Object o);

                      boolean containsAll(Collection<?> c);

                      //重复了怎么办
                      boolean addAll(Collection<? extends E> c);

                      //重复的会全弄走吗
                      boolean removeAll(Collection<?> c);

                      /*
                      删除此集合中满足给定谓词的所有元素。
                      在迭代期间或由谓词引发的错误或运行时异常将转发给调用者。
                      @return true if any elements were removed
                      */
                      default boolean removeIf(Predicate<? super E> filter) {
                      //非空filter
                      Objects.requireNonNull(filter);
                      boolean removed = false;
                      //迭代该集合
                      final Iterator<E> each = iterator();
                      while (each.hasNext()) {
                      if (filter.test(each.next())) {
                      each.remove();
                      removed = true;
                      }
                      }
                      return removed;
                      }

                      /*
                      把集合c中没有的元素全部移除
                      @return true if this collection changed as a result of the call
                      */
                      boolean retainAll(Collection<?> c);

                      void clear();

                      boolean equals(Object o);

                      /*
                      Any class that overrides the Object.equals method must also override the
                      Object.hashCode method.
                      c1.equals(c2) 相当于 c1.hashCode()==c2.hashCode().
                      */
                      int hashCode();

                      @Override
                      default Spliterator<E> spliterator() {
                      return Spliterators.spliterator(this, 0);
                      }

                      default Stream<E> stream() {
                      return StreamSupport.stream(spliterator(), false);
                      }

                      default Stream<E> parallelStream() {
                      return StreamSupport.stream(spliterator(), true);
                      }
                      }
                      + +

                      其中,

                        +
                      1. “Bags or multisets (unordered collections that may contain duplicate elements) should implement this interface directly.”

                        Set是不允许重复的元素集合的ADT,【ADT:抽象数据结构】

                        +

                        Bag是元素集合的ADT,允许重复.

                        +

                        通常,任何包含元素的东西都是Collection.

                        +

                        任何允许重复的集合都是Bag,否则就是Set.

                        +

                        通过索引访问元素的任何包都是List.

                        +

                        在最后一个之后附加新元素并且具有从头部(第一索引)移除元素的方法的Bag是Queue.

                        +

                        在最后一个之后附加新元素并且具有从尾部(最后一个索引)移除元素的方法的Bag是Stack.
                        ————————————————
                        原文链接:https://blog.csdn.net/weixin_34239718/article/details/114036886

                        +
                      2. +
                      3. “destructive” methods 和”undestructive” methods

                        这回答里写得很清楚:What are destructive and non-destructive methods in java?

                        +
                      4. +
                      5. recursive traversal of the collection
                        +

                        Some collection operations which perform recursive traversal of the collection may fail with an exception for self-referential instances where the collection directly or indirectly contains itself.

                        +
                        +

                        Java 8 vs Java 7 Collection Interface: self-referential instance

                        +

                        这个的第二个回答【较长的那个】写得很棒,较短的那个似乎是错误的。

                        +

                        正如描述所说的,“directly or indirectly contains itself”,回答里那个例子正是因为“indirectly contains itself”。

                        +

                        不仅仅是集合,每个Object都可能出现这样的错误(因为都包含有toString)

                        +

                        但是注意一点:

                        +
                        ArrayList l1 = new ArrayList();
                        l1.add(l1);
                        System.out.println(l1.toString());
                        //输出:[(this Collection)]
                        + +

                        这段代码是正常的,是因为ArrayList里面toString的实现:

                        +
                        sb.append('[');
                        for (;;) {
                        E e = it.next();
                        //注意此句
                        sb.append(e == this ? "(this Collection)" : e);
                        if (! it.hasNext())
                        return sb.append(']').toString();
                        sb.append(',').append(' ');
                        }
                        + +

                        规避了这种风险。

                        +

                        下面的hashcode是不正常的,因为hashcode实现没有规避这种风险。

                        +
                      6. +
                      7. 关于toArray的讨论
                        For toArray() :
                        The returned array will be "safe" in that no references to it are maintained by this collection. (In other words, this method must allocate a new array even if this collection is backed by an array). The caller is thus free to modify the returned array.
                        + +
                        List<String> list = Arrays.asList("foo", "bar", "baz");
                        String[] array = list.toArray(new String[0]);
                        array[0] = "qux";
                        //修改数组不会修改列表
                        System.out.println(list.get(0)); // still "foo"
                        list.set(0,"haha");
                        //修改列表也跟修改数组无关了
                        System.out.println(list.get(0)+array[0]);
                        + +
                        ArrayList<Student> a = new ArrayList<>();
                        a.add(new Student("Sarah",17));
                        Student[] s = a.toArray(new Student[0]);
                        //换一个引用对象
                        s[0]=new Student("Lily",20);
                        System.out.println(a.get(0)==s[0]);//false
                        + +
                        ArrayList<Student> a = new ArrayList<>();
                        a.add(new Student("Sarah",17));
                        Student[] s = a.toArray(new Student[0]);
                        //修改引用对象
                        s[0].name = "Lily";
                        System.out.println(a.get(0).name);//Lily
                        + +

                        这也就说明,toArray()实际上是把list的元素复制一份弄成array,直接把值粘贴进去。

                        +

                        对于引用对象,list的元素实际上应该存的是对象在堆中的地址。所谓的“安全”指的是,修改array中的元素的值【也即对象地址】,也就是换一个气球牵,是不会影响原来list的元素的值的。

                        +

                        因而,对于样例1和2,我们其实给array的元素换了个气球牵,或者是把list换了个气球牵,相互对象不同,没什么影响。

                        +

                        对于样例3,我们修改了list和array共同指向的对象【就像C语言的指针那样】

                        +

                        以上参考自What does “Safe” mean in the Collections.toArray() JavaDoc?

                        +
                      8. +
                      9. 关于default关键字

                        java中default关键字

                        +

                        starkoverflow关于为什么要设立default的讨论:

                        +

                        What is the purpose of the default keyword in Java

                        +

                        Default methods were added to Java 8 primarily to support lambda expressions.

                        +
                      10. +
                      +

                      AbstractCollection(A)

                      +

                      To implement an unmodifiable collection, the programmer needs only to extend this class and provide implementations for the iterator and size methods.
                      To implement a modifiable collection, the programmer must additionally[也要搞上面的] override this class’s add method (which otherwise throws an UnsupportedOperationException), and the iterator returned by the iterator method must additionally implement its remove method.

                      +
                      +
                      public abstract class AbstractCollection<E> implements Collection<E> {

                      //唯一的构造函数。 (用于子类构造函数的调用,通常是隐式的。)
                      protected AbstractCollection() {
                      }

                      // 查询操作
                      public abstract Iterator<E> iterator();
                      public abstract int size();
                      public boolean isEmpty() {
                      return size() == 0;
                      }

                      public boolean contains(Object o) {
                      Iterator<E> it = iterator();
                      if (o==null) {
                      while (it.hasNext())
                      if (it.next()==null)
                      return true;
                      } else {
                      while (it.hasNext())
                      if (o.equals(it.next()))
                      return true;
                      }
                      return false;
                      }


                      public Object[] toArray() {
                      // Estimate size of array; be prepared to see more or fewer elements
                      Object[] r = new Object[size()];
                      Iterator<E> it = iterator();
                      for (int i = 0; i < r.length; i++) {
                      if (! it.hasNext()) // fewer elements than expected
                      return Arrays.copyOf(r, i);
                      r[i] = it.next();
                      }
                      return it.hasNext() ? finishToArray(r, it) : r;
                      }


                      @SuppressWarnings("unchecked")
                      public <T> T[] toArray(T[] a) {
                      // Estimate size of array; be prepared to see more or fewer elements
                      int size = size();
                      //通过反射得到a同类型的数组实例
                      T[] r = a.length >= size ? a :
                      (T[])java.lang.reflect.Array
                      .newInstance(a.getClass().getComponentType(), size);
                      Iterator<E> it = iterator();

                      for (int i = 0; i < r.length; i++) {
                      if (! it.hasNext()) { // fewer elements than expected
                      if (a == r) {
                      r[i] = null; // null-terminate用null来标记数组结束。但如果数组里也有null该怎么办?从前面的contains来看也是有可能的
                      } else if (a.length < i) {
                      return Arrays.copyOf(r, i);
                      } else {
                      System.arraycopy(r, 0, a, 0, i);
                      if (a.length > i) {
                      a[i] = null;
                      }
                      }
                      return a;
                      }
                      r[i] = (T)it.next();
                      }
                      // more elements than expected
                      return it.hasNext() ? finishToArray(r, it) : r;
                      }

                      /*
                      为啥要-8?
                      Some VMs reserve some header words in an array. Attempts to allocate larger arrays may result in OutOfMemoryError: Requested array size exceeds VM limit
                      好像是因为要给8个字节保留作为数组的头标题。
                      */
                      private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;


                      //Reallocates the array being used within toArray when the iterator returned more elements than expected, and finishes filling it from the iterator.
                      @SuppressWarnings("unchecked")
                      private static <T> T[] finishToArray(T[] r, Iterator<?> it) {
                      int i = r.length;
                      while (it.hasNext()) {
                      int cap = r.length;
                      //说明此时需要扩容
                      if (i == cap) {
                      //每次扩当前大小的1/2
                      int newCap = cap + (cap >> 1) + 1;
                      // overflow-conscious code 溢出
                      if (newCap - MAX_ARRAY_SIZE > 0)
                      //cap+1 扩容后至少应该比原大小大1
                      newCap = hugeCapacity(cap + 1);
                      //申请一个船新数组空间
                      r = Arrays.copyOf(r, newCap);
                      }
                      r[i++] = (T)it.next();
                      }
                      // 如果过度扩容了就缩小回刚刚好
                      return (i == r.length) ? r : Arrays.copyOf(r, i);
                      }

                      private static int hugeCapacity(int minCapacity) {
                      //如果最小扩容也失败,说明要的东西太多了,救不了
                      if (minCapacity < 0) // overflow
                      throw new OutOfMemoryError
                      ("Required array size too large");
                      //否则
                      return (minCapacity > MAX_ARRAY_SIZE) ?
                      Integer.MAX_VALUE :
                      MAX_ARRAY_SIZE;
                      }

                      //修改操作

                      public boolean add(E e) {
                      throw new UnsupportedOperationException();
                      }

                      public boolean remove(Object o) {
                      Iterator<E> it = iterator();
                      if (o==null) {
                      while (it.hasNext()) {
                      if (it.next()==null) {
                      it.remove();
                      return true;
                      }
                      }
                      } else {
                      while (it.hasNext()) {
                      if (o.equals(it.next())) {
                      it.remove();
                      return true;
                      }
                      }
                      }
                      return false;
                      }

                      //批量操作
                      //O(n^2)
                      public boolean containsAll(Collection<?> c) {
                      for (Object e : c)
                      if (!contains(e))
                      return false;
                      return true;
                      }

                      public boolean addAll(Collection<? extends E> c) {
                      boolean modified = false;
                      for (E e : c)
                      if (add(e))
                      modified = true;
                      return modified;
                      }

                      public boolean removeAll(Collection<?> c) {
                      Objects.requireNonNull(c);
                      boolean modified = false;
                      Iterator<?> it = iterator();
                      while (it.hasNext()) {
                      if (c.contains(it.next())) {
                      it.remove();
                      modified = true;
                      }
                      }
                      return modified;
                      }

                      public boolean retainAll(Collection<?> c) {
                      Objects.requireNonNull(c);
                      boolean modified = false;
                      Iterator<E> it = iterator();
                      while (it.hasNext()) {
                      if (!c.contains(it.next())) {
                      it.remove();
                      modified = true;
                      }
                      }
                      return modified;
                      }

                      public void clear() {
                      Iterator<E> it = iterator();
                      while (it.hasNext()) {
                      it.next();
                      it.remove();
                      }
                      }

                      //字符串操作

                      public String toString() {
                      Iterator<E> it = iterator();
                      if (! it.hasNext())
                      return "[]";

                      StringBuilder sb = new StringBuilder();
                      sb.append('[');
                      for (;;) {
                      E e = it.next();
                      sb.append(e == this ? "(this Collection)" : e);
                      if (! it.hasNext())
                      return sb.append(']').toString();
                      sb.append(',').append(' ');
                      }
                      }
                      }
                      + +

                      其中:

                      +
                        +
                      1. 在toArray方法中,为什么需要写这么奇怪的代码?

                        what’s the usage of the code in the implementation of AbstractCollection’s toArray Method

                        +
                        Yes, you're right, as the javadoc sais, this method is prepared to return correctlly even if the Collection has been modified in the mean time.【并发安全】 That's why the initial size is just a hint. The usage of the iterator also ensures avoidance from the "concurrent modification" exception.
                      2. +
                      +

                      Queue

                      Queue(I)

                      + + +

                      The Queue interface does not define the blocking queue methods, which are common in concurrent programming. These methods, which wait for elements to appear or for space to become available, are defined in the java.util.concurrent.BlockingQueue interface, which extends this interface.

                      +

                      Queue implementations generally do not define element-based versions of methods equals and hashCode【就是不会像之前的list一样遍历一遍通过单个元素的hashcode计算整体的hashcode】 but instead inherit the identity based versions from class Object【hashcode由对象决定】, because element-based equality is not always well-defined for queues with the same elements but different ordering properties.

                      +

                      Each of these methods exists in two forms: one throws an exception if the operation fails, the other returns a special value (either null or false, depending on the operation). 容量受限的队列推荐使用第二种form

                      +
                      +

                      代码:

                      public interface Queue<E> extends Collection<E> {
                      /*
                      The offer method inserts an element if possible, otherwise returning false.
                      不同于Collection的add方法,offer添加失败时不会抛出异常,而是直接return false
                      */
                      boolean add(E e);
                      boolean offer(E e);
                      /*
                      The remove() and poll() methods differ only in their behavior
                      when the queue is empty:
                      the remove() method throws an exception, while the poll() method returns null.
                      */
                      E remove();
                      E poll();
                      /*
                      The element() and peek() methods return,
                      but do not remove, the head of the queue.
                      */
                      E element();
                      E peek();
                      }
                      + +

                      Deque(I)

                      +

                      双端队列。

                      +

                      The name deque is short for “double ended queue” and is usually pronounced “deck”.

                      + + +

                      This interface provides two methods to remove interior elements, removeFirstOccurrence and removeLastOccurrence.

                      + + + + + +
                      +

                      代码:

                      public interface Deque<E> extends Queue<E> {

                      void addFirst(E e);

                      void addLast(E e);

                      boolean offerFirst(E e);

                      boolean offerLast(E e);

                      E removeFirst();

                      E removeLast();

                      E pollFirst();

                      E pollLast();

                      E getFirst();

                      E getLast();

                      E peekFirst();

                      E peekLast();

                      boolean removeFirstOccurrence(Object o);

                      boolean removeLastOccurrence(Object o);

                      // *** Queue methods ***

                      boolean add(E e);

                      boolean offer(E e);

                      E remove();

                      E poll();

                      E element();

                      E peek();

                      // *** Stack methods ***

                      void push(E e);

                      E pop();

                      // *** Collection methods ***

                      boolean remove(Object o);

                      boolean contains(Object o);

                      public int size();

                      Iterator<E> iterator();
                      //Returns an iterator over the elements in this deque in reverse sequential order.
                      Iterator<E> descendingIterator();

                      }
                      + +

                      ArrayDeque

                      +

                      Resizable-array implementation of the Deque interface.

                      +

                      不允许空

                      +

                      Array deques have no capacity restrictions; they grow as necessary to support usage.

                      +

                      not thread-safe【相比于由vector实现的线程安全的Stack】

                      +

                      This class is likely to be faster than Stack when used as a stack, and faster than LinkedList when used as a queue.【6】

                      +

                      fail-fast

                      +
                      +

                      代码:

                      public class ArrayDeque<E> extends AbstractCollection<E>
                      implements Deque<E>, Cloneable, Serializable
                      {
                      //非private以让内部类能够访问到
                      /*
                      The capacity of the deque is the length of this array,
                      which is always a power of two. capacity只能是2的幂次
                      不能满【原理应该跟循环队列差不多,是怕头尾混淆】
                      但允许短暂的满之后马上扩容
                      所有不包含元素的数组单元为空
                      */
                      transient Object[] elements;

                      //The index of the element at the head of the deque
                      //(which is the element that would be removed by remove() or pop());
                      //or an arbitrary number equal to tail if the deque is empty.
                      //head在下标大的地方
                      transient int head;

                      //The index at which the 【next】 element would be added to the tail of the deque
                      //(via addLast(E), add(E), or push(E)).
                      //tail在下标小的地方
                      transient int tail;

                      private static final int MIN_INITIAL_CAPACITY = 8;

                      // ****** Array allocation and resizing utilities ******

                      private static int calculateSize(int numElements) {
                      int initialCapacity = MIN_INITIAL_CAPACITY;
                      // Find the best power of two to hold elements.
                      // Tests "<=" because arrays aren't kept full.
                      if (numElements >= initialCapacity) {
                      //原理类似HashMap.tableSizeFor
                      //这一通操作可以得到比cap大的,且离cap最近的2的幂次方数
                      initialCapacity = numElements;
                      initialCapacity |= (initialCapacity >>> 1);
                      initialCapacity |= (initialCapacity >>> 2);
                      initialCapacity |= (initialCapacity >>> 4);
                      initialCapacity |= (initialCapacity >>> 8);
                      initialCapacity |= (initialCapacity >>> 16);
                      initialCapacity++;

                      if (initialCapacity < 0) // Too many elements, must back off
                      initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
                      }
                      return initialCapacity;
                      }


                      private void allocateElements(int numElements) {
                      elements = new Object[calculateSize(numElements)];
                      }


                      private void doubleCapacity() {
                      assert head == tail;
                      int p = head;
                      int n = elements.length;
                      int r = n - p; // number of elements to the right of p
                      int newCapacity = n << 1;
                      if (newCapacity < 0)
                      throw new IllegalStateException("Sorry, deque too big");
                      Object[] a = new Object[newCapacity];
                      //以head==tail为分界线,右边那段移到开头,左边那段移到后面
                      System.arraycopy(elements, p, a, 0, r);
                      System.arraycopy(elements, 0, a, r, p);
                      elements = a;
                      head = 0;
                      tail = n;
                      }

                      private <T> T[] copyElements(T[] a) {
                      if (head < tail) {
                      System.arraycopy(elements, head, a, 0, size());
                      } else if (head > tail) {
                      int headPortionLen = elements.length - head;
                      System.arraycopy(elements, head, a, 0, headPortionLen);
                      System.arraycopy(elements, 0, a, headPortionLen, tail);
                      }
                      return a;
                      }

                      //1
                      public ArrayDeque() {
                      elements = new Object[16];
                      }

                      public ArrayDeque(int numElements) {
                      allocateElements(numElements);
                      }

                      public ArrayDeque(Collection<? extends E> c) {
                      allocateElements(c.size());
                      addAll(c);
                      }

                      // The main insertion and extraction methods are addFirst,
                      // addLast, pollFirst, pollLast. The other methods are defined in
                      // terms of these.就是说这几个最重要,别的方法都是这四个的附庸

                      public void addFirst(E e) {
                      if (e == null)
                      throw new NullPointerException();
                      //2
                      elements[head = (head - 1) & (elements.length - 1)] = e;
                      if (head == tail)
                      //队列满
                      doubleCapacity();
                      }

                      public void addLast(E e) {
                      if (e == null)
                      throw new NullPointerException();
                      elements[tail] = e;
                      if ( (tail = (tail + 1) & (elements.length - 1)) == head)
                      //队列满
                      doubleCapacity();
                      }

                      public boolean offerFirst(E e) {
                      addFirst(e);
                      return true;
                      }

                      public boolean offerLast(E e) {
                      addLast(e);
                      return true;
                      }

                      public E removeFirst() {
                      E x = pollFirst();
                      if (x == null)
                      throw new NoSuchElementException();
                      return x;
                      }

                      public E removeLast() {
                      E x = pollLast();
                      if (x == null)
                      throw new NoSuchElementException();
                      return x;
                      }

                      public E pollFirst() {
                      int h = head;
                      @SuppressWarnings("unchecked")
                      E result = (E) elements[h];
                      // Element is null if deque empty
                      if (result == null)
                      return null;
                      elements[h] = null; // Must null out slot
                      head = (h + 1) & (elements.length - 1);
                      return result;
                      }

                      public E pollLast() {
                      int t = (tail - 1) & (elements.length - 1);
                      @SuppressWarnings("unchecked")
                      E result = (E) elements[t];
                      if (result == null)
                      return null;
                      elements[t] = null;
                      tail = t;
                      return result;
                      }

                      public E getFirst() {
                      @SuppressWarnings("unchecked")
                      E result = (E) elements[head];
                      if (result == null)
                      throw new NoSuchElementException();
                      return result;
                      }

                      public E getLast() {
                      @SuppressWarnings("unchecked")
                      E result = (E) elements[(tail - 1) & (elements.length - 1)];
                      if (result == null)
                      throw new NoSuchElementException();
                      return result;
                      }

                      @SuppressWarnings("unchecked")
                      public E peekFirst() {
                      // elements[head] is null if deque empty
                      return (E) elements[head];
                      }

                      @SuppressWarnings("unchecked")
                      public E peekLast() {
                      return (E) elements[(tail - 1) & (elements.length - 1)];
                      }

                      public boolean removeFirstOccurrence(Object o) {
                      if (o == null)
                      return false;
                      //掩码
                      int mask = elements.length - 1;
                      int i = head;
                      Object x;
                      while ( (x = elements[i]) != null) {
                      if (o.equals(x)) {
                      delete(i);
                      return true;
                      }
                      //头->尾
                      i = (i + 1) & mask;
                      }
                      return false;
                      }

                      public boolean removeLastOccurrence(Object o) {
                      if (o == null)
                      return false;
                      int mask = elements.length - 1;
                      int i = (tail - 1) & mask;
                      Object x;
                      while ( (x = elements[i]) != null) {
                      if (o.equals(x)) {
                      delete(i);
                      return true;
                      }
                      //尾->头
                      i = (i - 1) & mask;
                      }
                      return false;
                      }

                      // *** Queue methods ***

                      public boolean add(E e) {
                      addLast(e);
                      return true;
                      }

                      public boolean offer(E e) {
                      return offerLast(e);
                      }

                      public E remove() {
                      return removeFirst();
                      }

                      public E poll() {
                      return pollFirst();
                      }

                      public E element() {
                      return getFirst();
                      }

                      public E peek() {
                      return peekFirst();
                      }

                      // *** Stack methods ***

                      public void push(E e) {
                      addFirst(e);
                      }

                      public E pop() {
                      return removeFirst();
                      }

                      //检查队列情况正常
                      private void checkInvariants() {
                      assert elements[tail] == null;
                      //如果成立,只能是队列空;不成立的话,不能有空元素
                      assert head == tail ? elements[head] == null :
                      (elements[head] != null &&
                      elements[(tail - 1) & (elements.length - 1)] != null);
                      assert elements[(head - 1) & (elements.length - 1)] == null;
                      }

                      //Returns: true if elements moved backwards而不是操作是否成功
                      private boolean delete(int i) {
                      checkInvariants();
                      final Object[] elements = this.elements;
                      final int mask = elements.length - 1;
                      final int h = head;
                      final int t = tail;
                      final int front = (i - h) & mask;
                      final int back = (t - i) & mask;

                      // Invariant: head <= i < tail mod circularity
                      if (front >= ((t - h) & mask))
                      throw new ConcurrentModificationException();

                      // Optimize for least element motion
                      if (front < back) {
                      if (h <= i) {
                      //把i覆盖掉了
                      System.arraycopy(elements, h, elements, h + 1, front);
                      } else { // Wrap around
                      System.arraycopy(elements, 0, elements, 1, i);
                      elements[0] = elements[mask];
                      System.arraycopy(elements, h, elements, h + 1, mask - h);
                      }
                      elements[h] = null;
                      head = (h + 1) & mask;
                      return false;
                      } else {
                      if (i < t) { // Copy the null tail as well
                      System.arraycopy(elements, i + 1, elements, i, back);
                      //注意没设置空,因为确实不用
                      tail = t - 1;
                      } else { // Wrap around
                      System.arraycopy(elements, i + 1, elements, i, mask - i);
                      elements[mask] = elements[0];
                      System.arraycopy(elements, 1, elements, 0, t);
                      tail = (t - 1) & mask;
                      }
                      return true;
                      }
                      }

                      // *** Collection Methods ***

                      public int size() {
                      return (tail - head) & (elements.length - 1);
                      }

                      public boolean isEmpty() {
                      return head == tail;
                      }

                      //The elements will be ordered from first (head) to last (tail).
                      //This is the same order that elements would be dequeued
                      //(via successive calls to remove or popped (via successive calls to pop).
                      public Iterator<E> iterator() {
                      return new DeqIterator();
                      }

                      public Iterator<E> descendingIterator() {
                      return new DescendingIterator();
                      }

                      private class DeqIterator implements Iterator<E> {

                      private int cursor = head;

                      //Tail recorded at construction (also in remove), to stop iterator[怪不得叫fence]
                      //and also to check for comodification[检查并发].
                      private int fence = tail;

                      private int lastRet = -1;

                      public boolean hasNext() {
                      return cursor != fence;
                      }

                      public E next() {
                      if (cursor == fence)
                      throw new NoSuchElementException();
                      @SuppressWarnings("unchecked")
                      E result = (E) elements[cursor];
                      // This check doesn't catch all possible comodifications,
                      // but does catch the ones that corrupt traversal【破坏遍历的】
                      //tail!=fence说明迭代时修改。
                      if (tail != fence || result == null)
                      throw new ConcurrentModificationException();
                      lastRet = cursor;
                      cursor = (cursor + 1) & (elements.length - 1);
                      return result;
                      }

                      public void remove() {
                      if (lastRet < 0)
                      throw new IllegalStateException();
                      //3
                      if (delete(lastRet)) { // if left-shifted, undo increment in next()
                      cursor = (cursor - 1) & (elements.length - 1);
                      //update
                      fence = tail;
                      }
                      lastRet = -1;
                      }

                      public void forEachRemaining(Consumer<? super E> action) {
                      Objects.requireNonNull(action);
                      Object[] a = elements;
                      int m = a.length - 1, f = fence, i = cursor;
                      //4执行完后直接迭代结束
                      cursor = f;
                      while (i != f) {
                      @SuppressWarnings("unchecked") E e = (E)a[i];
                      i = (i + 1) & m;
                      if (e == null)
                      throw new ConcurrentModificationException();
                      action.accept(e);
                      }
                      }
                      }

                      private class DescendingIterator implements Iterator<E> {

                      private int cursor = tail;
                      //终点为fence,此时终点为head
                      private int fence = head;
                      private int lastRet = -1;

                      public boolean hasNext() {
                      return cursor != fence;
                      }

                      public E next() {
                      if (cursor == fence)
                      throw new NoSuchElementException();
                      cursor = (cursor - 1) & (elements.length - 1);
                      @SuppressWarnings("unchecked")
                      E result = (E) elements[cursor];
                      if (head != fence || result == null)
                      throw new ConcurrentModificationException();
                      lastRet = cursor;
                      return result;
                      }

                      public void remove() {
                      if (lastRet < 0)
                      throw new IllegalStateException();
                      if (!delete(lastRet)) {
                      cursor = (cursor + 1) & (elements.length - 1);
                      fence = head;
                      }
                      lastRet = -1;
                      }
                      //不支持for-each了吧233
                      }

                      public boolean contains(Object o) {
                      if (o == null)
                      return false;
                      int mask = elements.length - 1;
                      int i = head;
                      Object x;
                      while ( (x = elements[i]) != null) {
                      if (o.equals(x))
                      return true;
                      i = (i + 1) & mask;
                      }
                      return false;
                      }

                      public boolean remove(Object o) {
                      return removeFirstOccurrence(o);
                      }

                      public void clear() {
                      int h = head;
                      int t = tail;
                      if (h != t) { // clear all cells
                      head = tail = 0;
                      int i = h;
                      int mask = elements.length - 1;
                      do {
                      elements[i] = null;
                      i = (i + 1) & mask;
                      } while (i != t);
                      }
                      }

                      public Object[] toArray() {
                      return copyElements(new Object[size()]);
                      }

                      @SuppressWarnings("unchecked")
                      public <T> T[] toArray(T[] a) {
                      int size = size();
                      if (a.length < size)
                      a = (T[])java.lang.reflect.Array.newInstance(
                      a.getClass().getComponentType(), size);
                      copyElements(a);
                      if (a.length > size)
                      a[size] = null;
                      return a;
                      }

                      // *** Object methods ***

                      public ArrayDeque<E> clone() {
                      try {
                      @SuppressWarnings("unchecked")
                      ArrayDeque<E> result = (ArrayDeque<E>) super.clone();
                      result.elements = Arrays.copyOf(elements, elements.length);
                      return result;
                      } catch (CloneNotSupportedException e) {
                      throw new AssertionError();
                      }
                      }

                      private static final long serialVersionUID = 2340985798034038923L;

                      private void writeObject(java.io.ObjectOutputStream s)
                      throws java.io.IOException {...}

                      private void readObject(java.io.ObjectInputStream s)
                      throws java.io.IOException, ClassNotFoundException {...}

                      public Spliterator<E> spliterator() {
                      return new DeqSpliterator<E>(this, -1, -1);
                      }

                      static final class DeqSpliterator<E> implements Spliterator<E> {...}

                      }
                      + +

                      其中:

                        +
                      1. 默认容量

                        空构造器的默认容量为16

                        +
                      2. +
                      3. (head - 1) & (elements.length - 1)

                        是一个便捷的截位取余操作,这跟hashmap一个原理,详见hashmap第二点。

                        +
                      4. +
                      5. if (delete(lastRet))

                        delete方法返回true,说明右移数组,此时next指针需要++

                        +

                        delete方法返回false,说明左移数组,此时next指针不变

                        +
                      6. +
                      7. forEachRemaining

                        跟差不多所有的迭代器实现一样,此方法执行完毕之后,cursor直接跳到数组最末,相当于迭代结束

                        +
                      8. +
                      +

                      List

                      List(I)

                      有序、支持随机访问

                      +

                      代码

                      /*
                      List 接口提供了一个特殊的迭代器,称为 ListIterator,
                      除了 Iterator 接口提供的正常操作之外,它还允许元素插入和替换以及双向访问。
                      List 接口提供了两种方法来搜索指定的对象。 从性能的角度来看,应谨慎使用这些方法。
                      在许多实现中,它们将执行代价高昂的线性搜索。
                      Note: While it is permissible for lists to contain themselves as elements, extreme caution is advised: the equals and hashCode methods are no longer well defined on such a list.【这跟上面的引用点4是一样的。】

                      */
                      public interface List<E> extends Collection<E> {
                      int size();
                      boolean isEmpty();
                      boolean contains(Object o);
                      Iterator<E> iterator();
                      Object[] toArray();
                      <T> T[] toArray(T[] a);
                      boolean add(E e);
                      boolean remove(Object o);
                      boolean containsAll(Collection<?> c);
                      boolean addAll(Collection<? extends E> c);
                      boolean removeAll(Collection<?> c);
                      boolean retainAll(Collection<?> c);
                      //Returns true if and only if the specified object is also a list,
                      //both lists have the same size, and all corresponding pairs of elements in the two lists are equal.
                      //In other words, two lists are defined to be equal if they contain the same elements in the same order.
                      //注意,对于未重载equal方法的类,引用对象的相等指的是地址相等也就是说必须是一模一样的对象,两个不同对象但值相同,这种情况是不算equal的。
                      boolean equals(Object o);
                      int hashCode();

                      //newly add or change below

                      //将此列表的每个元素替换为将运算符应用于该元素的结果。
                      default void replaceAll(UnaryOperator<E> operator) {
                      Objects.requireNonNull(operator);
                      final ListIterator<E> li = this.listIterator();
                      while (li.hasNext()) {
                      li.set(operator.apply(li.next()));
                      }
                      }

                      //@SuppressWarings注解 作用:用于抑制编译器产生警告信息。
                      @SuppressWarnings({"unchecked", "rawtypes"})
                      default void sort(Comparator<? super E> c) {
                      Object[] a = this.toArray();
                      //借助Arrays的sort方法
                      Arrays.sort(a, (Comparator) c);
                      ListIterator<E> i = this.listIterator();
                      //再线性逐一替换
                      for (Object e : a) {
                      i.next();
                      // set:
                      // Replaces the last element returned by next or previous
                      // with the specified element
                      i.set((E) e);
                      }
                      }

                      E get(int index);

                      E set(int index, E element);

                      //插入元素,把当前位及其以后的元素都往后挪一位
                      void add(int index, E element);

                      //移除元素,把当前位及其以后的元素都往前挪一位
                      E remove(int index);

                      //-1 if this list does not contain the element
                      //ClassCastException:if the type of the specified element
                      //is incompatible with this list
                      int indexOf(Object o);

                      ListIterator<E> listIterator();

                      //指定的索引指示初始调用next将返回的第一个元素.对 previous 的初始调用将返回具有指定索引减一的元素。
                      ListIterator<E> listIterator(int index);

                      //[fromIndex,toIndex).If fromIndex==toIndex then return==null.
                      List<E> subList(int fromIndex, int toIndex);

                      @Override
                      default Spliterator<E> spliterator() {
                      return Spliterators.spliterator(this, Spliterator.ORDERED);
                      }

                      }
                      + +

                      其中:

                        +
                      1. hashcode不允许自环

                        Note: While it is permissible for lists to contain themselves as elements, extreme caution is advised: the equals and hashCode methods are no longer well defined on such a list.【这跟上面的引用点4是差不多的。】

                        +
                        ArrayList l1 = new ArrayList();
                        l1.add(l1);
                        System.out.println(l1.hashCode());
                        //输出:Stack Overflow
                      2. +
                      3. structural and non-structural change in list

                        Difference between structural and non-structural for lists

                        +
                      4. +
                      5. 关于sublist
                        ① view

                        sublist的document有一个很有趣的点,就是把sublist称为原list的视图view,这不禁让人想起了数据库里的表和表的视图。

                        +

                        但是还是有差别的。

                        +

                        在数据库中,视图仅仅是表或表的一部分的快照,修改视图对原表没有影响。但此处,对sublist的结构性修改和非结构性修改都会使原list的对应元素发生改变。

                        +

                        但有一点是相同的。如果对原表/原list修改,那么视图就会没用/会寄掉。

                        +

                        可以像这样来对主list指定范围内的元素进行操作,免去复杂的下标。

                        +
                        //For example, the following idiom removes a range of elements from a list:
                        list.subList(from, to).clear();
                        + +
                        ② 留下一个问题
                        public class Main {
                        public static void main(String[] args) {
                        ArrayList<Student> a=new ArrayList<>();
                        a.add(new Student("Lily",16));
                        a.add(new Student("Sam",16));
                        a.add(new Student("Tom",16));
                        a.add(new Student("Mary",16));
                        a.add(new Student("Mark",16));
                        a.add(new Student("John",16));

                        List<Student> suba=a.subList(2,5);

                        System.out.println("Firstly,suba:");
                        printall(suba);

                        System.out.println("Change the value of age:");
                        suba.get(1).age=300;
                        System.out.println("suba:");
                        printall(suba);
                        System.out.println("a:");
                        printall(a);

                        System.out.println("Change the address:");
                        suba.set(1,new Student("haha",200));
                        System.out.println("suba:");
                        printall(suba);
                        System.out.println("a:");
                        printall(a);

                        System.out.println("Change the structure of sublist:");
                        suba.remove(0);
                        System.out.println("suba:");
                        printall(suba);
                        System.out.println("a:");
                        printall(a);
                        }
                        public static void printall(List<Student> list){
                        for (Student a : list){
                        System.out.print(a+"\t");
                        }
                        System.out.println();
                        }
                        }
                        /*运行结果
                        Firstly,suba:
                        name :Tomage :16 name :Maryage :16 name :Markage :16
                        Change the value of age:
                        suba:
                        name :Tomage :16 name :Maryage :300 name :Markage :16
                        a:
                        name :Lilyage :16 name :Samage :16 name :Tomage :16 name :Maryage :300 name :Markage :16 name :Johnage :16
                        Change the address:
                        suba:
                        name :Tomage :16 name :hahaage :200 name :Markage :16
                        a:
                        name :Lilyage :16 name :Samage :16 name :Tomage :16 name :hahaage :200 name :Markage :16 name :Johnage :16
                        Change the structure of sublist:
                        suba:
                        name :hahaage :200 name :Markage :16
                        a:
                        name :Lilyage :16 name :Samage :16 name :hahaage :200 name :Markage :16 name :Johnage :16
                        */
                        + +

                        为什么之前的toArray得到的array换了个对象,原list不会变【详见Collection说明第五点】;而这里的sublist得到的子list换了个对象,原list也会变呢?

                        +

                        也许这跟间接寻址的级数有关?之后看过ArrayList的具体实现再来解答。

                        +
                        +

                        穿越回来:

                        +

                        ​ toArray得到array,是新开辟了存储空间,里面存放了原list对象的地址。因而,修改新存储空间的地址内容,原list是不变的。

                        +

                        ​ 但sublist连新开辟存储空间也没有,其差不多所有操作都是从list进行的。因而它换了个对象,具体实现就是让原list也换了个对象。

                        +

                        ​ 所以说其实前者更像是数据库里面的“视图”概念。

                        +
                        +
                        ③ 关于sublis和原list、non-structral和structural的区别:

                        Can structural changes in sublist reflected in the original list, in JAVA?

                        +
                        +

                        In the document of List class:

                        +
                        /*
                        The returned list is backed by this list, so non-structural changes in the returned list are reflected in this list, and vice-versa.
                        */
                        List<E> subList(int fromIndex, int toIndex);
                        + +

                        Can structural changes in sublist reflected in the original list in JAVA?

                        +
                        +

                        答案是会改变的。实例可看问题中代码,或者point3的代码。

                        +

                        但是既然都会变的话,为什么文档里面要特意强调“non-structural”呢?这是因为,对sublist进行结构性改变会让原list也正常地一起变,但是对原list进行结构性改变却会让sublist寄掉:

                        +

                        文档下面接着写道:

                        +
                        The semantics of the list returned by this method become undefined if the backing list (i.e., this list) is structurally modified in any way other than via the returned list. (Structural modifications are those that change the size of this list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results.)
                        /*
                        sublist会寄,如果其原list被结构性改变了,且是以除了通过sublist结构性改变的方式外的其他方式改变的。(结构性改变是指那些会改变list的长度,或者会使迭代失败的那些改变)
                        */
                        + +

                        通过代码验证可得,确实寄了

                        +
                        public static void main(String[] args) {
                        ArrayList<Student> a=new ArrayList<>();
                        a.add(new Student("Lily",16));
                        a.add(new Student("Sam",16));
                        a.add(new Student("Tom",16));
                        a.add(new Student("Mary",16));
                        a.add(new Student("Mark",16));
                        a.add(new Student("John",16));

                        List<Student> suba=a.subList(2,5);

                        System.out.println("Firstly,suba:");
                        printall(suba);
                        printall(a);

                        System.out.println("Change the origin structure:");
                        a.remove(3);
                        System.out.println("a:");
                        printall(a);
                        System.out.println("suba:");
                        printall(suba);
                        }
                        /*运行结果
                        Firstly,suba:
                        name :Tomage :16 name :Maryage :16 name :Markage :16
                        name :Lilyage :16 name :Samage :16 name :Tomage :16 name :Maryage :16 name :Markage :16 name :Johnage :16
                        Change the origin structure:
                        a:
                        name :Lilyage :16 name :Samage :16 name :Tomage :16 name :Markage :16 name :Johnage :16
                        suba:
                        Exception in thread "main" java.util.ConcurrentModificationException
                        at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1241)
                        */
                        + +

                        通过以上可以感觉,估计sublist和原list的元素是共享存储空间的,只不过可能对象里有相关维护的变量。Any method accessing the list through the sub list effectively does index + offset.故而要是list变了,sublist的相关维护变量不变,就会依然傻傻地进行offset+index操作,这样就会寄。此猜想有待验证23333

                        -

                        世界上只有13个根DNS服务器IP地址,但是有很多台根DNS服务器。

                        -
                        -

                        主机需要手动配置DNS服务器地址。

                        -

                        当浏览器需要填写请求头时,它需要通过系统调用向操作系统发送DNS查询请求。操作系统将DNS请求发送给配置在主机上的DNS服务器(下称A),A再向根DNS服务器发送请求。根DNS服务器解析域名,返回下一级DNS服务器的IP地址。A再向下级DNS服务器再次发送请求,下级再返回下下级IP地址。以此类推,最终A就能得到目标IP地址的正确响应。整个过程如下图所示:

                        -

                        image-20231010132718116

                        -

                        与此同时,各个DNS服务器都会有定时刷新的缓存,从而加速了查找效率。

                        -

                        用电信号传输TCP/IP数据

                        TCP/IP

                        本章前面大多讨论TCP/IP具体协议内容,以前已经了解过很多次了就不多赘述。所以TCP/IP部分就以分点的形式随意列举一下:

                        -
                          -
                        1. IP 中还包括 ICMPA 协议和 ARPB 协议。ICMP 用于告知网络包传送过程中产生的错误以及各种控制消息,ARP 用于根据 IP 地址查询相应的以太网 MAC 地址。

                          -
                        2. -
                        3. 套接字中记录了用于控制通信操作的各种控制信息,协议栈则需要根据这些信息判断下一步的行动,【包括应用程序信息和协议栈状态信息】这就是套接字的作用。所以需要针对不同协议栈实现不同的socket。

                          -
                        4. -
                        5. 是的,回想当初CS144,也是socket来负责有特定消息时调用TCP相关函数来通知处理。

                          -
                        6. -
                        7. 连接 connect

                          -

                          连接实际上是通信双方交换控制信息,在套接字中记录这些必要信息并准备数据收发的一连串操作。

                          +

                          穿越回来:

                            -
                          1. 应用程序向协议栈传ip地址
                          2. -
                          3. 本机向服务器发通信请求
                          4. -
                          5. 过程中分配通信缓冲区
                          6. +
                          7. 确实是共享存储空间的,不如说sublist就直接引用了原list的变量,所有操作实质上都是在原list上进行。
                          8. +
                          9. sublist傻傻地进行offset+index操作,这样体现在原list上,可能导致下标越界或者结果并非我们想要的。
                          -
                        8. -
                        9. 动态调整等待时间

                          -

                          image-20231012113725655

                          +
                        -

                        以太网

                          -
                        1. 以太网的定义

                          -

                          image-20231012113840636

                          -
                        2. -
                        3. 系统初始化时MAC地址的设置

                          -

                          MAC地址是上电后由驱动程序从ROM中读取的,而非自动获取的

                          -
                        4. -
                        5. 电信号转换【这个帅得不行】

                          -

                          为了区分连续的1或0,我们就需要同时发送数据信号和时钟信号,然而这样开销太大,因而我们引入了上升沿

                          -

                          上升沿本质上是数据信号和时钟信号叠加而成的结果,叠加方式是异或

                          -

                          image-20231012114006378

                          -

                          提到异或是否感觉豁然开朗?是的,这东西恢复时也是使用了异或的性质:接收方从帧头获取时钟频率从而得到时钟信号,跟收到的叠加信号进行再次叠加(异或),就可以获得原来的数据信号了。

                          -

                          我只能说牛逼,一直以来对异或的视角还停留在单纯的数字,这个波形的物理概念真的惊到我了。

                          -

                          实例:

                          -

                          image-20231012114350599

                          -
                        6. -
                        7. 半双工模式【同一时刻只能进行发or收】使用集线器,全双工模式【发or收可以并行】使用交换机。半双工模式需要进行载波监听碰撞检测。

                          -
                        8. -
                        9. 服务器的操作系统具备和路由器相同的包转发功能,当打开这一功能时,它就可以像路由器一样对包进行转发。在这种情况下,当收到不是发给自己的包的时候,就会像路由器一样执行包转发操作。

                          +

                          AbstractList(A)

                          +

                          提供随机访问list的基本骨架

                          +

                          To implement an unmodifiable list, the programmer needs only to extend this class and provide implementations for the get(int) and size() methods.

                          +

                          To implement a modifiable list, the programmer must additionally override the set(int, Object) method

                          +

                          If the list is variable-size,the programmer must additionally override the add(int, E) and remove(int) methods.

                          +

                          Unlike the other abstract collection implementations, the programmer does not have to provide an iterator implementation; the iterator and list iterator are implemented by this class, on top of the “random access“ methods: get(int), set(int, E), add(int, E) and remove(int).这里的迭代器反而是通过类里面的方法来实现的

                          +
                          +

                          代码

                          public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {

                          protected AbstractList() {
                          }
                          //重写add方法
                          public boolean add(E e) {
                          //此为下面的public void add(int index, E element);
                          add(size(), e);
                          return true;
                          }
                          //交给具体实现
                          abstract public E get(int index);
                          //假设不能修改
                          public E set(int index, E element) {
                          throw new UnsupportedOperationException();
                          }
                          public void add(int index, E element) {
                          throw new UnsupportedOperationException();
                          }
                          public E remove(int index) {
                          throw new UnsupportedOperationException();
                          }

                          // Search Operations

                          public int indexOf(Object o) {
                          ListIterator<E> it = listIterator();
                          if (o==null) {
                          while (it.hasNext())
                          if (it.next()==null)
                          return it.previousIndex();
                          } else {
                          while (it.hasNext())
                          if (o.equals(it.next()))
                          return it.previousIndex();
                          }
                          return -1;
                          }

                          public int lastIndexOf(Object o) {
                          //从后往前遍历
                          ListIterator<E> it = listIterator(size());
                          if (o==null) {
                          while (it.hasPrevious())
                          if (it.previous()==null)
                          return it.nextIndex();
                          } else {
                          while (it.hasPrevious())
                          if (o.equals(it.previous()))
                          return it.nextIndex();
                          }
                          return -1;
                          }

                          // Bulk Operations

                          public void clear() {
                          removeRange(0, size());
                          }

                          public boolean addAll(int index, Collection<? extends E> c) {
                          rangeCheckForAdd(index);
                          boolean modified = false;
                          for (E e : c) {
                          add(index++, e);
                          modified = true;
                          }
                          return modified;
                          }

                          // Iterators

                          public Iterator<E> iterator() {
                          return new Itr();
                          }


                          public ListIterator<E> listIterator() {
                          return listIterator(0);
                          }


                          public ListIterator<E> listIterator(final int index) {
                          rangeCheckForAdd(index);

                          return new ListItr(index);
                          }

                          //内部类诶 1
                          private class Itr implements Iterator<E> {
                          //Index of element to be returned by subsequent call to next.
                          int cursor = 0;
                          //Index of element returned by most recent call to next or previous. Reset to -1 if this element is deleted by a call to remove.
                          int lastRet = -1;

                          int expectedModCount = modCount;

                          public boolean hasNext() {
                          return cursor != size();
                          }

                          public E next() {
                          checkForComodification();
                          try {
                          int i = cursor;
                          E next = get(i);
                          lastRet = i;
                          cursor = i + 1;
                          return next;
                          } catch (IndexOutOfBoundsException e) {
                          checkForComodification();
                          throw new NoSuchElementException();
                          }
                          }

                          public void remove() {
                          if (lastRet < 0)
                          throw new IllegalStateException();
                          checkForComodification();

                          try {
                          AbstractList.this.remove(lastRet);
                          if (lastRet < cursor)
                          cursor--;
                          lastRet = -1;
                          expectedModCount = modCount;
                          } catch (IndexOutOfBoundsException e) {
                          throw new ConcurrentModificationException();
                          }
                          }

                          final void checkForComodification() {
                          if (modCount != expectedModCount)
                          throw new ConcurrentModificationException();
                          }
                          }

                          private class ListItr extends Itr implements ListIterator<E> {
                          ListItr(int index) {
                          cursor = index;
                          }

                          public boolean hasPrevious() {
                          return cursor != 0;
                          }

                          public E previous() {
                          checkForComodification();
                          try {
                          int i = cursor - 1;
                          E previous = get(i);
                          //lastRet=i;cursor=i;
                          lastRet = cursor = i;
                          return previous;
                          } catch (IndexOutOfBoundsException e) {
                          checkForComodification();
                          throw new NoSuchElementException();
                          }
                          }

                          public int nextIndex() {
                          return cursor;
                          }

                          public int previousIndex() {
                          return cursor-1;
                          }

                          public void set(E e) {
                          if (lastRet < 0)
                          throw new IllegalStateException();
                          checkForComodification();

                          try {
                          AbstractList.this.set(lastRet, e);
                          expectedModCount = modCount;
                          } catch (IndexOutOfBoundsException ex) {
                          throw new ConcurrentModificationException();
                          }
                          }

                          public void add(E e) {
                          checkForComodification();

                          try {
                          int i = cursor;
                          AbstractList.this.add(i, e);
                          lastRet = -1;
                          cursor = i + 1;
                          expectedModCount = modCount;
                          } catch (IndexOutOfBoundsException ex) {
                          throw new ConcurrentModificationException();
                          }
                          }
                          }

                          //迭代器定义结束

                          public List<E> subList(int fromIndex, int toIndex) {
                          //Sublist和RandomAccessSubList在后面都作为内部类定义
                          //这俩的分支主要是能否支持高性能随机访问,而这点在Java是依靠是否实现RandomAccess接口
                          //来体现的,要实现接口必须得是一个类。因此,为了分歧,这里不得不创建两个类来表示两种情况。
                          //这两个类的方法应该是大致相同的。
                          //2
                          return (this instanceof RandomAccess ?
                          new RandomAccessSubList<>(this, fromIndex, toIndex) :
                          new SubList<>(this, fromIndex, toIndex));
                          }

                          // Comparison and hashing

                          public boolean equals(Object o) {
                          if (o == this)
                          return true;
                          if (!(o instanceof List))
                          return false;

                          ListIterator<E> e1 = listIterator();
                          ListIterator<?> e2 = ((List<?>) o).listIterator();
                          while (e1.hasNext() && e2.hasNext()) {
                          E o1 = e1.next();
                          Object o2 = e2.next();
                          //如果这东西没有重载equals方法,那此处就是单纯object对象是否相同了
                          if (!(o1==null ? o2==null : o1.equals(o2)))
                          return false;
                          }
                          //为啥size不一样不在一开始就比呢?那样不是更省花销吗
                          return !(e1.hasNext() || e2.hasNext());
                          }

                          public int hashCode() {
                          int hashCode = 1;
                          for (E e : this)
                          hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
                          return hashCode;
                          }

                          protected void removeRange(int fromIndex, int toIndex) {
                          ListIterator<E> it = listIterator(fromIndex);
                          for (int i=0, n=toIndex-fromIndex; i<n; i++) {
                          it.next();
                          it.remove();
                          }
                          }

                          //The number of times this list has been structurally modified.
                          //3
                          protected transient int modCount = 0;

                          private void rangeCheckForAdd(int index) {
                          if (index < 0 || index > size())
                          throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
                          }

                          private String outOfBoundsMsg(int index) {
                          return "Index: "+index+", Size: "+size();
                          }
                          }


                          class SubList<E> extends AbstractList<E> {
                          //这大概是对父list的引用
                          private final AbstractList<E> l;
                          //这大概是在父list的起始偏移量
                          private final int offset;
                          //sublist的长度
                          private int size;

                          SubList(AbstractList<E> list, int fromIndex, int toIndex) {
                          if (fromIndex < 0)
                          throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
                          if (toIndex > list.size())
                          throw new IndexOutOfBoundsException("toIndex = " + toIndex);
                          if (fromIndex > toIndex)
                          throw new IllegalArgumentException("fromIndex(" + fromIndex +
                          ") > toIndex(" + toIndex + ")");
                          l = list;
                          offset = fromIndex;
                          size = toIndex - fromIndex;
                          this.modCount = l.modCount;
                          }

                          public E set(int index, E element) {
                          rangeCheck(index);
                          checkForComodification();
                          //改变sublist也会改变父list,是否成功取决于对父list的改变是否成功
                          return l.set(index+offset, element);
                          }

                          public E get(int index) {
                          rangeCheck(index);
                          checkForComodification();
                          //直接取父类值
                          return l.get(index+offset);
                          }

                          public int size() {
                          checkForComodification();
                          return size;
                          }

                          public void add(int index, E element) {
                          rangeCheckForAdd(index);
                          checkForComodification();
                          l.add(index+offset, element);
                          this.modCount = l.modCount;
                          size++;
                          }

                          public E remove(int index) {
                          rangeCheck(index);
                          checkForComodification();
                          E result = l.remove(index+offset);
                          this.modCount = l.modCount;
                          size--;
                          return result;
                          }

                          protected void removeRange(int fromIndex, int toIndex) {
                          checkForComodification();
                          l.removeRange(fromIndex+offset, toIndex+offset);
                          this.modCount = l.modCount;
                          size -= (toIndex-fromIndex);
                          }

                          public boolean addAll(Collection<? extends E> c) {
                          return addAll(size, c);
                          }

                          public boolean addAll(int index, Collection<? extends E> c) {
                          rangeCheckForAdd(index);
                          int cSize = c.size();
                          if (cSize==0)
                          return false;

                          checkForComodification();
                          l.addAll(offset+index, c);
                          this.modCount = l.modCount;
                          size += cSize;
                          return true;
                          }

                          public Iterator<E> iterator() {
                          return listIterator();
                          }

                          public ListIterator<E> listIterator(final int index) {
                          checkForComodification();
                          rangeCheckForAdd(index);

                          //改变迭代器实现
                          return new ListIterator<E>() {
                          //子类迭代器=父类迭代器+offset
                          private final ListIterator<E> i = l.listIterator(index+offset);

                          public boolean hasNext() {
                          return nextIndex() < size;
                          }

                          public E next() {
                          if (hasNext())
                          return i.next();
                          else
                          throw new NoSuchElementException();
                          }

                          public boolean hasPrevious() {
                          return previousIndex() >= 0;
                          }

                          public E previous() {
                          if (hasPrevious())
                          return i.previous();
                          else
                          throw new NoSuchElementException();
                          }

                          public int nextIndex() {
                          return i.nextIndex() - offset;
                          }

                          public int previousIndex() {
                          return i.previousIndex() - offset;
                          }

                          public void remove() {
                          i.remove();
                          SubList.this.modCount = l.modCount;
                          size--;
                          }

                          public void set(E e) {
                          i.set(e);
                          }

                          public void add(E e) {
                          i.add(e);
                          SubList.this.modCount = l.modCount;
                          size++;
                          }
                          };
                          }

                          //层层套娃啊
                          public List<E> subList(int fromIndex, int toIndex) {
                          return new SubList<>(this, fromIndex, toIndex);
                          }

                          private void rangeCheck(int index) {
                          if (index < 0 || index >= size)
                          throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
                          }

                          private void rangeCheckForAdd(int index) {
                          if (index < 0 || index > size)
                          throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
                          }

                          private String outOfBoundsMsg(int index) {
                          return "Index: "+index+", Size: "+size;
                          }

                          private void checkForComodification() {
                          if (this.modCount != l.modCount)
                          throw new ConcurrentModificationException();
                          }
                          }

                          //确实实现上没什么区别,主要是多了个RandomAccess的约定。
                          class RandomAccessSubList<E> extends SubList<E> implements RandomAccess {
                          RandomAccessSubList(AbstractList<E> list, int fromIndex, int toIndex) {
                          super(list, fromIndex, toIndex);
                          }

                          public List<E> subList(int fromIndex, int toIndex) {
                          return new RandomAccessSubList<>(this, fromIndex, toIndex);
                          }
                          }

                          + +

                          其中:

                            +
                          1. 内部类ListItr和Itr的实现

                            Itr的实现需要用到AbstratcList类的get和set方法,而显然不同Collection的get和set不一样。为了避免混淆,Itr就只能作为私有类。为了避免胡乱引用,Itr就可以直接作为内部类,共享其外部类的所有资源。

                            +

                            ListItr作为内部私有类很容易理解,毕竟只有list才需要它。

                          2. -
                          -]]> - - books - - - - 记录一次vm扩容 - /2023/09/27/%E8%AE%B0%E5%BD%95%E4%B8%80%E6%AC%A1vm%E6%89%A9%E5%AE%B9/ - 扩容

                          容量寄,但是df -h发现这次好像是不大一样的:

                          -

                          image-20230927191332920

                          -

                          查了一下,原来这是逻辑卷管理(LVM,Logical Volume Manger)。

                          +
                        10. RandomAccess

                          RandomAccess是一个空接口,它应该代表一个约定俗成的规定,即它的implementations的随机访问都是性能较高的。这个空接口思想很常见,源码带给我们的智慧。

                          +
                          按经验来说, a List implementation should implement this interface, if this loop:
                          for (int i=0, n=list.size(); i < n; i++)
                          list.get(i);

                          runs faster than this loop:
                          for (Iterator i=list.iterator(); i.hasNext(); )
                          i.next();
                        11. +
                        12. 关于modCount字段与fail-fast机制

                          你真的知道集合中modCount字段作用吗?

                          +

                          modCount字段就是保证一定程度并发安全的变量,fail-fast就是指马上抛出异常。

                          + + +

                          modCount不能保证绝对的并发安全,因为它只负责防范结构改变,而不负责看某位置的数据更新。

                          +

                          在现实中要实现对集合的边迭代边修改,下面三种方式都是错的:

                          + + +

                          第一种方法也可以把for里的size换成list.size()

                          +

                          其中乐观锁与悲观锁可见:乐观锁

                          -

                          参考

                          -

                          实现将多个硬盘和硬盘分区做成一个逻辑卷,并将逻辑卷统一管理。创建LVM顺序为:物理卷PV->卷组VG->逻辑卷LV。
                          物理卷(PV,Physical Volume):物理硬盘或分区;
                          卷组(VG,Volume Group):一个或多个物理卷集合;
                          逻辑卷(Logical Volume):供系统使用的元设备,虚拟分区。

                          -

                          LVM常用的命令:

                          - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
                          功能PV物理卷管理VG卷组管理LV逻辑卷管理
                          扫描pvscanvgscanlvscan
                          创建pvcreatevgcreatelvcreate
                          查看pvdisplayvgdisplaylvdisplay
                          删除pvremovevgremovelvremove
                          扩展/vgextendlvextend
                          缩容/vgreducelvreduce
                          +
                            +
                          • 乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只在更新的时候会判断一下在此期间别人有没有去更新这个数据。
                          • +
                          +
                          +

                          注意“在此期间”的含义是拿到数据到更新数据的这段时间。因为没有加锁,所以别的线程可能会更改。还有一点那就是乐观锁其实是不加锁的来保证某个变量一系列操作原子性的一种方法。

                          +
                          +
                            +
                          • 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞,直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。
                          • +
                          -

                          接下来简要介绍其扩容步骤。

                          -
                            -
                          1. 在vmware中扩展磁盘容量

                            -

                            image-20230927191636392

                            -
                          2. -
                          3. sudo fdisk /dev/sda,进行磁盘分区

                            -

                            在fdisk中输入n,新建sda4分区,然后w保存。

                          4. -
                          5. 执行下列命令:

                            -
                            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的时候报错,然后之后又不小心重启了,最后的最后只能重装。。。又是一晚上配环境。。。。

                          -]]> - - - 阅读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 %}

                          +

                          AbstractList帮我们实现了差不多所有方法,除了Tget(int)size()set(int, Object)add(int, E)remove(int) 。因而,接下来的两个实现中,重点关注这些就行。

                          +

                          ArrayList

                          代码

                          +

                          Implements all optional list operations, and permits all elements, including null.

                          +

                          This class is roughly equivalent to Vector, except that it is unsynchronized.

                          +

                          size(),isEmpty(),get(),set(),iterator(),listIterator()的时间复杂符是**常量级别(constant time),add()方法的时间复杂度是可变常量级别(amortized constant time),即为O(n)。大致上说,剩余方法的时间复杂度都是线性时间(linear time)。相较于LinkedList的实现,ArrayList的常量因子(constant factor)**较低。

                          +

                          An application can increase the capacity of an ArrayList instance before adding a large number of elements using the ensureCapacity operation. This may reduce the amount of incremental reallocation.

                          +

                          本身是线程不安全的,但可以通过封装类来实现线程同步。可以使用Collections.synchronizedList method.

                          +
                          List list = Collections.synchronizedList(new ArrayList(...));
                          -

                          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的那个是一样的】

                          +

                          总体来说实现的很多方法跟想象中差别不大,有几个比较惊艳

                          +
                          //1 cloneable
                          public class ArrayList<E> extends AbstractList<E>
                          implements List<E>, RandomAccess, Cloneable, java.io.Serializable
                          {
                          //4
                          private static final long serialVersionUID = 8683452581122892189L;

                          //可以注意一下,初始=10.
                          private static final int DEFAULT_CAPACITY = 10;

                          //Shared empty array instance used for empty instances.
                          //7
                          private static final Object[] EMPTY_ELEMENTDATA = {};

                          private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

                          //5、6
                          /*
                          The array buffer into which the elements of the ArrayList are stored.缓冲
                          ArrayList的容量=此数组的length
                          Any empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA will be expanded to DEFAULT_CAPACITY when the first element is added.
                          */
                          transient Object[] elementData; // non-private to simplify nested class access

                          private int size;

                          public ArrayList(int initialCapacity) {
                          if (initialCapacity > 0) {
                          this.elementData = new Object[initialCapacity];
                          } else if (initialCapacity == 0) {
                          this.elementData = EMPTY_ELEMENTDATA;
                          } else {
                          throw new IllegalArgumentException("Illegal Capacity: "+
                          initialCapacity);
                          }
                          }

                          public ArrayList() {
                          this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
                          }

                          //in the order they are returned by c's iterator.
                          public ArrayList(Collection<? extends E> c) {
                          Object[] a = c.toArray();
                          if ((size = a.length) != 0) {
                          if (c.getClass() == ArrayList.class) {
                          elementData = a;
                          } else {
                          elementData = Arrays.copyOf(a, size, Object[].class);
                          }
                          //size==0
                          } else {
                          // replace with empty array.
                          elementData = EMPTY_ELEMENTDATA;
                          }
                          }

                          //把容量缩小为当前size。An application can use this operation to minimize the storage of an ArrayList instance.
                          public void trimToSize() {
                          //涉及List的结构性改变
                          modCount++;
                          if (size < elementData.length) {
                          elementData = (size == 0)
                          ? EMPTY_ELEMENTDATA
                          : Arrays.copyOf(elementData, size);
                          }
                          }

                          //8
                          public void ensureCapacity(int minCapacity) {
                          int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                          //如果原size!=0,除非minCapacity=0,否则必须是要扩容一次的【函数要求】,
                          //因此设置为最小值0,以确保下面的if条件一定为true。
                          ? 0
                          //如果为默认大小0,此处是必须扩为默认大小的
                          : DEFAULT_CAPACITY;

                          if (minCapacity > minExpand) {
                          ensureExplicitCapacity(minCapacity);
                          }
                          }

                          private static int calculateCapacity(Object[] elementData, int minCapacity) {
                          if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                          return Math.max(DEFAULT_CAPACITY, minCapacity);
                          }
                          return minCapacity;
                          }

                          private void ensureCapacityInternal(int minCapacity) {
                          ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
                          }

                          private void ensureExplicitCapacity(int minCapacity) {
                          //修改list结构
                          //若minCapacity<elementData.length,本句modCount++始终执行。
                          modCount++;
                          // overflow-conscious code
                          if (minCapacity - elementData.length > 0)
                          grow(minCapacity);
                          }

                          private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

                          private void grow(int minCapacity) {
                          // overflow-conscious code
                          //capacity=length of elementData
                          int oldCapacity = elementData.length;
                          //每次扩1/2
                          int newCapacity = oldCapacity + (oldCapacity >> 1);
                          if (newCapacity - minCapacity < 0)
                          newCapacity = minCapacity;
                          //防止溢出
                          if (newCapacity - MAX_ARRAY_SIZE > 0)
                          newCapacity = hugeCapacity(minCapacity);
                          // minCapacity is usually close to size, so this is a win:
                          elementData = Arrays.copyOf(elementData, newCapacity);
                          }

                          private static int hugeCapacity(int minCapacity) {
                          if (minCapacity < 0) // overflow
                          throw new OutOfMemoryError();
                          return (minCapacity > MAX_ARRAY_SIZE) ?
                          Integer.MAX_VALUE :
                          MAX_ARRAY_SIZE;
                          }

                          public int size() {
                          return size;
                          }

                          public boolean isEmpty() {
                          return size == 0;
                          }

                          public boolean contains(Object o) {
                          return indexOf(o) >= 0;
                          }

                          public int indexOf(Object o) {
                          if (o == null) {
                          for (int i = 0; i < size; i++)
                          if (elementData[i]==null)
                          return i;
                          } else {
                          for (int i = 0; i < size; i++)
                          if (o.equals(elementData[i]))
                          return i;
                          }
                          return -1;
                          }

                          public int lastIndexOf(Object o) {
                          if (o == null) {
                          for (int i = size-1; i >= 0; i--)
                          if (elementData[i]==null)
                          return i;
                          } else {
                          for (int i = size-1; i >= 0; i--)
                          if (o.equals(elementData[i]))
                          return i;
                          }
                          return -1;
                          }
                          //2
                          public Object clone() {
                          try {
                          //from Object.clone():
                          //the returned object should be obtained by calling super.clone.
                          ArrayList<?> v = (ArrayList<?>) super.clone();
                          //浅拷贝,只拷贝地址
                          v.elementData = Arrays.copyOf(elementData, size);
                          v.modCount = 0;
                          return v;
                          } catch (CloneNotSupportedException e) {
                          // this shouldn't happen, since we are Cloneable
                          throw new InternalError(e);
                          }
                          }

                          public Object[] toArray() {
                          return Arrays.copyOf(elementData, size);
                          }

                          @SuppressWarnings("unchecked")
                          public <T> T[] toArray(T[] a) {
                          if (a.length < size)
                          // Make a new array of a's runtime type, but my contents:
                          return (T[]) Arrays.copyOf(elementData, size, a.getClass());
                          System.arraycopy(elementData, 0, a, 0, size);
                          if (a.length > size)
                          a[size] = null;
                          return a;
                          }

                          // Positional Access Operations

                          @SuppressWarnings("unchecked")
                          E elementData(int index) {
                          return (E) elementData[index];
                          }

                          //这里我挺迷惑的,为什么还要再套一层elementData??
                          public E get(int index) {
                          rangeCheck(index);

                          return elementData(index);
                          }

                          public E set(int index, E element) {
                          rangeCheck(index);

                          E oldValue = elementData(index);
                          elementData[index] = element;
                          return oldValue;
                          }

                          public boolean add(E e) {
                          //既增加了modcount,也保证了capacity够用
                          ensureCapacityInternal(size + 1);
                          elementData[size++] = e;
                          return true;
                          }

                          public void add(int index, E element) {
                          rangeCheckForAdd(index);
                          ensureCapacityInternal(size + 1); // Increments modCount!!
                          System.arraycopy(elementData, index, elementData, index + 1,
                          size - index);//src dest 移动数组
                          elementData[index] = element;
                          size++;
                          }

                          public E remove(int index) {
                          rangeCheck(index);

                          modCount++;
                          E oldValue = elementData(index);

                          int numMoved = size - index - 1;
                          if (numMoved > 0)
                          System.arraycopy(elementData, index+1, elementData, index,
                          numMoved);
                          elementData[--size] = null; // clear to let GC do its work,自动清除无引用对象

                          return oldValue;
                          }

                          public boolean remove(Object o) {
                          if (o == null) {
                          for (int index = 0; index < size; index++)
                          if (elementData[index] == null) {
                          fastRemove(index);
                          return true;
                          }
                          } else {
                          for (int index = 0; index < size; index++)
                          if (o.equals(elementData[index])) {
                          fastRemove(index);
                          return true;
                          }
                          }
                          return false;
                          }

                          //这不是跟上面一模一样吗,为啥还要再写一遍?
                          private void fastRemove(int index) {
                          modCount++;
                          int numMoved = size - index - 1;
                          if (numMoved > 0)
                          System.arraycopy(elementData, index+1, elementData, index,
                          numMoved);
                          elementData[--size] = null; // clear to let GC do its work
                          }

                          public void clear() {
                          modCount++;

                          //9 我能不能直接猛一点:elementData=new Object[elementData.length]?
                          // clear to let GC do its work
                          for (int i = 0; i < size; i++)
                          elementData[i] = null;

                          size = 0;
                          }

                          public boolean addAll(Collection<? extends E> c) {
                          Object[] a = c.toArray();
                          int numNew = a.length;
                          ensureCapacityInternal(size + numNew); // Increments modCount
                          System.arraycopy(a, 0, elementData, size, numNew);
                          size += numNew;
                          return numNew != 0;
                          }

                          public boolean addAll(int index, Collection<? extends E> c) {
                          rangeCheckForAdd(index);

                          Object[] a = c.toArray();
                          int numNew = a.length;
                          ensureCapacityInternal(size + numNew); // Increments modCount

                          int numMoved = size - index;
                          if (numMoved > 0)
                          System.arraycopy(elementData, index, elementData, index + numNew,
                          numMoved);

                          System.arraycopy(a, 0, elementData, index, numNew);
                          size += numNew;
                          return numNew != 0;
                          }

                          protected void removeRange(int fromIndex, int toIndex) {
                          modCount++;
                          int numMoved = size - toIndex;
                          System.arraycopy(elementData, toIndex, elementData, fromIndex,
                          numMoved);

                          // clear to let GC do its work
                          int newSize = size - (toIndex-fromIndex);
                          for (int i = newSize; i < size; i++) {
                          elementData[i] = null;
                          }
                          size = newSize;
                          }

                          private void rangeCheck(int index) {
                          if (index >= size)
                          throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
                          }

                          private void rangeCheckForAdd(int index) {
                          if (index > size || index < 0)
                          throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
                          }

                          private String outOfBoundsMsg(int index) {
                          return "Index: "+index+", Size: "+size;
                          }

                          public boolean removeAll(Collection<?> c) {
                          Objects.requireNonNull(c);
                          return batchRemove(c, false);
                          }

                          public boolean retainAll(Collection<?> c) {
                          Objects.requireNonNull(c);
                          return batchRemove(c, true);
                          }

                          //这个complement做得很漂亮,兼顾实际意义又统一了代码
                          private boolean batchRemove(Collection<?> c, boolean complement) {
                          //局部变量
                          final Object[] elementData = this.elementData;
                          int r = 0, w = 0;
                          boolean modified = false;

                          try {
                          for (; r < size; r++)
                          if (c.contains(elementData[r]) == complement)
                          //原地平移,nice
                          //如果是removeall,此条件成立说明c不含有该元素,则保留该元素
                          //如果是retainall,此条件成立说明c含有该元素,则保留该元素
                          elementData[w++] = elementData[r];
                          } finally {
                          // Preserve behavioral compatibility with AbstractCollection,
                          // even if c.contains() throws.
                          if (r != size) {
                          System.arraycopy(elementData, r,
                          elementData, w,
                          size - r);
                          w += size - r;
                          }
                          if (w != size) {
                          // clear to let GC do its work
                          for (int i = w; i < size; i++)
                          elementData[i] = null;
                          modCount += size - w;
                          size = w;
                          modified = true;
                          }
                          }
                          return modified;
                          }

                          //序列化
                          private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
                          // Write out element count, and any hidden stuff
                          int expectedModCount = modCount;
                          /*from ObjectOutputStream:
                          Write the non-static and non-transient fields of the current class
                          to this stream.*/
                          s.defaultWriteObject();

                          //为啥要特地强调write size?
                          s.writeInt(size);

                          for (int i=0; i<size; i++) {
                          s.writeObject(elementData[i]);
                          }

                          //保证一定基础的序列化同步
                          if (modCount != expectedModCount) {
                          throw new ConcurrentModificationException();
                          }
                          }

                          private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
                          elementData = EMPTY_ELEMENTDATA;
                          // Read in size, and any hidden stuff
                          s.defaultReadObject();
                          //不大懂为什么这里的值被ignore了?
                          // Read in capacity
                          s.readInt(); // ignored

                          if (size > 0) {
                          // be like clone(), allocate array based upon size not capacity
                          int capacity = calculateCapacity(elementData, size);
                          SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
                          ensureCapacityInternal(size);

                          Object[] a = elementData;
                          // Read in all elements in the proper order.
                          for (int i=0; i<size; i++) {
                          a[i] = s.readObject();
                          }
                          }
                          }

                          public ListIterator<E> listIterator(int index) {
                          if (index < 0 || index > size)
                          throw new IndexOutOfBoundsException("Index: "+index);
                          return new ListItr(index);
                          }

                          public ListIterator<E> listIterator() {
                          return new ListItr(0);
                          }

                          public Iterator<E> iterator() {
                          return new Itr();
                          }

                          /**
                          * AbstractList.Itr的优化版本
                          */
                          private class Itr implements Iterator<E> {
                          int cursor; // index of next element to return
                          int lastRet = -1; // index of last element returned; -1 if no such
                          int expectedModCount = modCount;

                          Itr() {}

                          public boolean hasNext() {
                          return cursor != size;
                          }

                          @SuppressWarnings("unchecked")
                          public E next() {
                          checkForComodification();
                          int i = cursor;
                          if (i >= size)
                          throw new NoSuchElementException();
                          //内部类访问外部类this的方法
                          Object[] elementData = ArrayList.this.elementData;
                          if (i >= elementData.length)
                          throw new ConcurrentModificationException();
                          cursor = i + 1;
                          return (E) elementData[lastRet = i];
                          }

                          public void remove() {
                          if (lastRet < 0)
                          throw new IllegalStateException();
                          checkForComodification();

                          try {
                          //change here
                          //确实AbstractList的那个remove应该更适用于LinkedList
                          ArrayList.this.remove(lastRet);
                          cursor = lastRet;
                          lastRet = -1;
                          expectedModCount = modCount;
                          } catch (IndexOutOfBoundsException ex) {
                          throw new ConcurrentModificationException();
                          }
                          }

                          @Override
                          @SuppressWarnings("unchecked")
                          public void forEachRemaining(Consumer<? super E> consumer) {
                          Objects.requireNonNull(consumer);
                          final int size = ArrayList.this.size;
                          int i = cursor;
                          //这里不抛异常吗
                          if (i >= size) {
                          return;
                          }
                          final Object[] elementData = ArrayList.this.elementData;
                          //好像确实是并发修改了,毕竟上面已经test过i<size<capacity了
                          if (i >= elementData.length) {
                          throw new ConcurrentModificationException();
                          }
                          while (i != size && modCount == expectedModCount) {
                          consumer.accept((E) elementData[i++]);
                          }
                          // update once at end of iteration to reduce heap write traffic
                          //10
                          cursor = i;
                          lastRet = i - 1;
                          checkForComodification();
                          }

                          final void checkForComodification() {
                          if (modCount != expectedModCount)
                          throw new ConcurrentModificationException();
                          }
                          }

                          /**
                          * AbstractList.ListItr的优化版本
                          */
                          private class ListItr extends Itr implements ListIterator<E> {
                          ListItr(int index) {
                          super();
                          cursor = index;
                          }

                          public boolean hasPrevious() {
                          return cursor != 0;
                          }

                          public int nextIndex() {
                          return cursor;
                          }

                          public int previousIndex() {
                          return cursor - 1;
                          }

                          @SuppressWarnings("unchecked")
                          public E previous() {
                          checkForComodification();
                          int i = cursor - 1;
                          if (i < 0)
                          throw new NoSuchElementException();
                          Object[] elementData = ArrayList.this.elementData;
                          //11 防止并发修改,比如在这期间进行了trim
                          if (i >= elementData.length)
                          throw new ConcurrentModificationException();
                          cursor = i;
                          return (E) elementData[lastRet = i];
                          }

                          public void set(E e) {
                          if (lastRet < 0)
                          throw new IllegalStateException();
                          checkForComodification();

                          try {
                          ArrayList.this.set(lastRet, e);
                          } catch (IndexOutOfBoundsException ex) {
                          throw new ConcurrentModificationException();
                          }
                          }

                          public void add(E e) {
                          checkForComodification();

                          try {
                          int i = cursor;
                          ArrayList.this.add(i, e);
                          cursor = i + 1;
                          lastRet = -1;
                          expectedModCount = modCount;
                          } catch (IndexOutOfBoundsException ex) {
                          throw new ConcurrentModificationException();
                          }
                          }
                          }

                          public List<E> subList(int fromIndex, int toIndex) {
                          subListRangeCheck(fromIndex, toIndex, size);
                          return new SubList(this, 0, fromIndex, toIndex);
                          }

                          static void subListRangeCheck(int fromIndex, int toIndex, int size) {
                          if (fromIndex < 0)
                          throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
                          if (toIndex > size)
                          throw new IndexOutOfBoundsException("toIndex = " + toIndex);
                          if (fromIndex > toIndex)
                          throw new IllegalArgumentException("fromIndex(" + fromIndex +
                          ") > toIndex(" + toIndex + ")");
                          }

                          //依然内部类。不过这里应该用了个作用域的性质。相比于AbstractList定义在List类外部的
                          //Sublist,应该会更优先使用定义在内部的Sublist
                          //12 注意,这里的sublist没有直接extends ArrayList
                          private class SubList extends AbstractList<E> implements RandomAccess {
                          private final AbstractList<E> parent;
                          //新增成员
                          private final int parentOffset;
                          private final int offset;
                          int size;

                          SubList(AbstractList<E> parent,
                          int offset, int fromIndex, int toIndex) {
                          this.parent = parent;
                          //这个parentOffset应该是指相较于最近的父亲的偏移量,offset应该就是相较于最底层的父亲的偏移量
                          this.parentOffset = fromIndex;
                          this.offset = offset + fromIndex;
                          this.size = toIndex - fromIndex;
                          this.modCount = ArrayList.this.modCount;
                          }

                          public E set(int index, E e) {
                          rangeCheck(index);
                          checkForComodification();
                          E oldValue = ArrayList.this.elementData(offset + index);
                          ArrayList.this.elementData[offset + index] = e;
                          return oldValue;
                          }

                          public E get(int index) {
                          rangeCheck(index);
                          checkForComodification();
                          return ArrayList.this.elementData(offset + index);
                          }

                          public int size() {
                          checkForComodification();
                          return this.size;
                          }

                          public void add(int index, E e) {
                          rangeCheckForAdd(index);
                          checkForComodification();
                          //13
                          parent.add(parentOffset + index, e);
                          this.modCount = parent.modCount;
                          this.size++;
                          }

                          public E remove(int index) {
                          rangeCheck(index);
                          checkForComodification();
                          E result = parent.remove(parentOffset + index);
                          this.modCount = parent.modCount;
                          this.size--;
                          return result;
                          }

                          protected void removeRange(int fromIndex, int toIndex) {
                          checkForComodification();
                          parent.removeRange(parentOffset + fromIndex,
                          parentOffset + toIndex);
                          this.modCount = parent.modCount;
                          this.size -= toIndex - fromIndex;
                          }

                          public boolean addAll(Collection<? extends E> c) {
                          return addAll(this.size, c);
                          }

                          public boolean addAll(int index, Collection<? extends E> c) {
                          rangeCheckForAdd(index);
                          int cSize = c.size();
                          if (cSize==0)
                          return false;

                          checkForComodification();
                          parent.addAll(parentOffset + index, c);
                          this.modCount = parent.modCount;
                          this.size += cSize;
                          return true;
                          }

                          public Iterator<E> iterator() {
                          return listIterator();
                          }

                          public ListIterator<E> listIterator(final int index) {
                          checkForComodification();
                          rangeCheckForAdd(index);
                          final int offset = this.offset;

                          return new ListIterator<E>() {
                          int cursor = index;
                          int lastRet = -1;
                          int expectedModCount = ArrayList.this.modCount;

                          public boolean hasNext() {
                          //内部内部类还可以访问外部类和外部外部类
                          return cursor != SubList.this.size;
                          }

                          @SuppressWarnings("unchecked")
                          public E next() {
                          checkForComodification();
                          int i = cursor;
                          if (i >= SubList.this.size)
                          throw new NoSuchElementException();
                          Object[] elementData = ArrayList.this.elementData;
                          if (offset + i >= elementData.length)
                          throw new ConcurrentModificationException();
                          cursor = i + 1;
                          return (E) elementData[offset + (lastRet = i)];
                          }

                          public boolean hasPrevious() {
                          return cursor != 0;
                          }

                          @SuppressWarnings("unchecked")
                          public E previous() {
                          checkForComodification();
                          int i = cursor - 1;
                          if (i < 0)
                          throw new NoSuchElementException();
                          Object[] elementData = ArrayList.this.elementData;
                          if (offset + i >= elementData.length)
                          throw new ConcurrentModificationException();
                          cursor = i;
                          return (E) elementData[offset + (lastRet = i)];
                          }

                          @SuppressWarnings("unchecked")
                          public void forEachRemaining(Consumer<? super E> consumer) {
                          Objects.requireNonNull(consumer);
                          final int size = SubList.this.size;
                          int i = cursor;
                          if (i >= size) {
                          return;
                          }
                          final Object[] elementData = ArrayList.this.elementData;
                          if (offset + i >= elementData.length) {
                          throw new ConcurrentModificationException();
                          }
                          while (i != size && modCount == expectedModCount) {
                          consumer.accept((E) elementData[offset + (i++)]);
                          }
                          // update once at end of iteration to reduce heap write traffic
                          lastRet = cursor = i;
                          checkForComodification();
                          }

                          public int nextIndex() {
                          return cursor;
                          }

                          public int previousIndex() {
                          return cursor - 1;
                          }

                          public void remove() {
                          if (lastRet < 0)
                          throw new IllegalStateException();
                          checkForComodification();

                          try {
                          SubList.this.remove(lastRet);
                          cursor = lastRet;
                          lastRet = -1;
                          expectedModCount = ArrayList.this.modCount;
                          } catch (IndexOutOfBoundsException ex) {
                          //确实要是IndexOutOfBounds的话,就说明lastRet改了,说明Concurrent了
                          throw new ConcurrentModificationException();
                          }
                          }

                          public void set(E e) {
                          if (lastRet < 0)
                          throw new IllegalStateException();
                          checkForComodification();

                          try {
                          ArrayList.this.set(offset + lastRet, e);
                          } catch (IndexOutOfBoundsException ex) {
                          throw new ConcurrentModificationException();
                          }
                          }

                          public void add(E e) {
                          checkForComodification();

                          try {
                          int i = cursor;
                          SubList.this.add(i, e);
                          cursor = i + 1;
                          lastRet = -1;
                          expectedModCount = ArrayList.this.modCount;
                          } catch (IndexOutOfBoundsException ex) {
                          throw new ConcurrentModificationException();
                          }
                          }

                          final void checkForComodification() {
                          if (expectedModCount != ArrayList.this.modCount)
                          throw new ConcurrentModificationException();
                          }
                          };
                          }

                          public List<E> subList(int fromIndex, int toIndex) {
                          subListRangeCheck(fromIndex, toIndex, size);
                          return new SubList(this, offset, fromIndex, toIndex);
                          }

                          public Spliterator<E> spliterator() {
                          checkForComodification();
                          return new ArrayListSpliterator<E>(ArrayList.this, offset,
                          offset + this.size, this.modCount);
                          }
                          }

                          @Override
                          public void forEach(Consumer<? super E> action) {
                          Objects.requireNonNull(action);
                          final int expectedModCount = modCount;
                          @SuppressWarnings("unchecked")
                          final E[] elementData = (E[]) this.elementData;
                          final int size = this.size;
                          //一修改就会寄
                          for (int i=0; modCount == expectedModCount && i < size; i++) {
                          action.accept(elementData[i]);
                          }
                          if (modCount != expectedModCount) {
                          throw new ConcurrentModificationException();
                          }
                          }

                          @Override
                          public Spliterator<E> spliterator() {
                          return new ArrayListSpliterator<>(this, 0, -1, 0);
                          }

                          /** Index-based split-by-two, lazily initialized Spliterator */
                          //基于索引的二分法,懒加载的 Spliterator
                          static final class ArrayListSpliterator<E> implements Spliterator<E> {...}

                          @Override
                          public boolean removeIf(Predicate<? super E> filter) {
                          Objects.requireNonNull(filter);

                          int removeCount = 0;
                          //666,用了类似掩码的思想,这样就能避免多次移动数组了,实现O(n),很不错
                          final BitSet removeSet = new BitSet(size);
                          final int expectedModCount = modCount;
                          final int size = this.size;
                          for (int i=0; modCount == expectedModCount && i < size; i++) {
                          @SuppressWarnings("unchecked")
                          final E element = (E) elementData[i];
                          if (filter.test(element)) {
                          //set:Sets the bit at the specified index to true.
                          removeSet.set(i);
                          removeCount++;
                          }
                          }
                          if (modCount != expectedModCount) {
                          throw new ConcurrentModificationException();
                          }

                          // shift surviving elements left over the spaces left by removed elements
                          final boolean anyToRemove = removeCount > 0;
                          if (anyToRemove) {
                          final int newSize = size - removeCount;
                          for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
                          //nextClearBit:Returns the index of the first bit that is set to false
                          //false表示不移走,true表示移走
                          i = removeSet.nextClearBit(i);
                          elementData[j] = elementData[i];
                          }
                          for (int k=newSize; k < size; k++) {
                          elementData[k] = null; // Let gc do its work
                          }
                          this.size = newSize;
                          if (modCount != expectedModCount) {
                          throw new ConcurrentModificationException();
                          }
                          modCount++;
                          }

                          return anyToRemove;
                          }

                          @Override
                          @SuppressWarnings("unchecked")
                          public void replaceAll(UnaryOperator<E> operator) {
                          Objects.requireNonNull(operator);
                          final int expectedModCount = modCount;
                          final int size = this.size;
                          for (int i=0; modCount == expectedModCount && i < size; i++) {
                          elementData[i] = operator.apply((E) elementData[i]);
                          }
                          if (modCount != expectedModCount) {
                          throw new ConcurrentModificationException();
                          }
                          modCount++;
                          }

                          @Override
                          @SuppressWarnings("unchecked")
                          public void sort(Comparator<? super E> c) {
                          final int expectedModCount = modCount;
                          Arrays.sort((E[]) elementData, 0, size, c);
                          if (modCount != expectedModCount) {
                          throw new ConcurrentModificationException();
                          }
                          modCount++;
                          }
                          }
                          + +

                          其中:

                            +
                          1. Cloneable接口

                            与RandomAccess一样,都是一个规定性质的接口。

                            +
                            +

                            A class implements the Cloneable interface to indicate to the Object.clone() method that it is legal for that method to make a field-for-field copy of instances of that class.

                            +

                            Classes that implement this interface should override Object.clone (which is protected) with a public method.

                            -

                            map没有迭代器

                            - +
                          2. +
                          3. clone与浅拷贝(shallow copy)
                                public static void main(String[] args) {
                            ArrayList<Student> arr = new ArrayList<>();
                            arr.add(new Student("lylt",15));
                            ArrayList<Student> cl = (ArrayList)arr.clone();
                            System.out.println(cl.get(0).toString());
                            cl.get(0).name="Sam";
                            System.out.println(cl.get(0).toString());
                            System.out.println(arr.get(0).toString());
                            cl.set(0,new Student("HWX",19));
                            System.out.println(cl.get(0).toString());
                            System.out.println(arr.get(0).toString());
                            }
                            /*运行结果:
                            name :lyltage :15
                            name :Samage :15
                            name :Samage :15
                            name :HWXage :19
                            name :Samage :15
                            */
                            -

                            代码:

                            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 迭代
                            }
                            +

                            结合内部代码可知,确实跟上面那个toArray的原理是一样的,只拷贝地址。

                            +

                            clone是浅拷贝。

                            +

                            浅拷贝与深拷贝的区别

                            +
                          4. +
                          5. 关于子类继承到的父类内部类

                            本来在犹豫,子类默认继承到的内部类里面用到的外部类方法的版本是取父还是取子,经过以下实验可知,是取能访问到的最新版本。

                            +
                            public class Main {
                            public static void main(String[] args) {
                            ChildOuter chldot = new ChildOuter();
                            chldot.printname();
                            chldot.in.printall();
                            }
                            }
                            /*结果:子类声明为private的成员字段不能被从父类继承而来的方法访问到
                            Father
                            I am father!*/
                            class FatherOuter{
                            String name="Father";
                            private void print(){
                            System.out.println("I am father!");
                            }
                            public Inner in = new Inner();
                            public class Inner{
                            public int haha=1;
                            void printall(){
                            print();
                            }
                            }
                            public void printname(){
                            System.out.println(name);
                            }
                            }

                            class ChildOuter extends FatherOuter{
                            private String name="Child";
                            private void print(){System.out.println("I am child!");}
                            }
                            -

                            其中:

                              -
                            1. get return null时的情况

                              get return null when value==NULL or key不存在。

                              -

                              为了区分这两种情况,写代码时可以用:

                              -
                              if !containsKey(key){
                              key不存在
                              }
                              Obj obj=get(key);
                              +

                              如若把Father和Child内的print类都换成public:

                              +
                              class FatherOuter{
                              //...
                              public void print(){
                              System.out.println("I am father!");
                              }
                              //...
                              }

                              class ChildOuter extends FatherOuter{
                              //...
                              public void print(){System.out.println("I am child!");}
                              }
                              /*结果:访问最新版本
                              I am child!*/
                              -

                              其实源码中的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.

                              +
                              //结论:内部类可以访问外部类所有不论私有还是公有的资源;会优先访问最新版本【父子类而言】
                              //子类声明为private的成员字段不能被从父类继承而来的方法访问到,只会访问能访问到的最新版本
                            4. +
                            5. 序列化versionID

                              ArrayList实现了java.io.Serializable接口,故而可以被序列化和反序列化,就需要有个序列化版本ID

                              +

                              What is a serialVersionUID and why should I use it?

                              +
                              +

                              这东西是用来在反序列化的时候保证收到的对象和发送的对象的类是相同的。If the receiver has loaded a class for the object that has a different serialVersionUID than that of the corresponding sender’s class, then deserialization will result in an InvalidClassException.

                              +

                              The field serialVersionUID must be static, final, and of type long.

                              +

                              If a serializable class does not explicitly declare a serialVersionUID, then the serialization runtime will calculate a default serialVersionUID value for that class based on various aspects of the class, as described in the Java(TM) Object Serialization Specification.

                              -

                              如下代码测试:

                              -
                                  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进行遍历。

                            6. -
                            7. 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
                              +
                            8. transient

                              Java中transient关键字的详细总结

                              +
                              +

                              transient是短暂的意思。对于transient 修饰的成员变量,在类的实例对象的序列化处理过程中会被忽略。 因此,transient变量不会贯穿对象的序列化和反序列化,生命周期仅存于调用者的内存中而不会写到磁盘里进行持久化。

                              +

                              在持久化对象时,对于一些特殊的数据成员(如用户的密码,银行卡号等),我们不想用序列化机制来保存它。为了在一个特定对象的一个成员变量上关闭序列化,可以在这个成员变量前加上关键字transient。

                              +

                              注意static修饰的静态变量天然就是不可序列化的。一个静态变量不管是否被transient修饰,均不能被序列化(如果反序列化后类中static变量还有值,则值为当前JVM中对应static变量的值)。序列化保存的是对象状态,静态变量保存的是类状态,因此序列化并不保存静态变量。

                              +

                              使用场景
                              (1)类中的字段值可以根据其它字段推导出来,如一个长方形类有三个属性长度、宽度、面积,面积不需要序列化。
                              (2) 一些安全性的信息,一般情况下是不能离开JVM的。
                              (3)如果类中使用了Logger实例,那么Logger实例也是不需要序列化的

                              -

                              相比于其它的view,多了第二句话

                              +

                              但其实还有一个问题,它这边源码对这个transient的注释是:non-private to simplify nested class access,“非私有以简化嵌套类访问”。问题是这个transient和类的公有还是私有有什么关系呢?

                              +

                              Why is the data array in java.util.ArrayList package-private?

                              +

                              其实没怎么看懂23333

                            9. -
                            10. 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)));
                              +
                            11. ArrayList的elementData虽然被transient修饰,但仍然能够序列化

                              ArrayList中elementData为何被transient修饰?

                              +
                            12. +
                            13. 关于static的空数组

                              EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA这两个空数组可用于表示两种情况:

                              +

                              new ArrayList(0) ->EMPTY_ELEMENTDATA

                              +

                              new ArrayList() ->DEFAULTCAPACITY_EMPTY_ELEMENTDATA

                              +

                              之所以用静态,是提取了共性:不论是需要什么ArrayList,其空形态不都一样吗(

                              +

                              这样可以避免了制造对象的浪费。very good。

                              +
                            14. +
                            15. 关于扩容的连环计

                              我其实觉得不必写那么麻烦……

                              +

                              In the JAVA ArrayList source code, why does array expansion should be divided into ensureCapacityInternal and ensureCapacity two sides?

                              +

                              经过测试替换可得,确实可以像我那样写。

                              +
                              		//此处的ArrayList是魔改过的
                              //now the capacity is 0
                              ArrayList a = new ArrayList<>();
                              System.out.println(a.getCapacity());
                              //扩容到DEFAULT
                              a.add(new Student("Lily",15));
                              System.out.println(a.getCapacity());
                              addStudent(a);
                              System.out.println(a.getCapacity());
                              /*运行结果:
                              0
                              10
                              33
                              */
                            16. +
                            17. 关于clear我的写法

                              In the Java ArrayList source code, in the clear function, can my rewrite more efficient than the origin source code?

                              +
                              ArrayList a = new ArrayList<>();
                              addStudent(a);
                              long sum1=0;
                              for (int i=0;i<50000;i++){
                              long startTime = System.currentTimeMillis();
                              a.clear1();
                              addStudent(a);
                              long endTime = System.currentTimeMillis();
                              sum1+=endTime-startTime;
                              }
                              //sum1/=500;
                              System.out.println("clear1: "+sum1);
                              sum1=0;
                              for (int i=0;i<50000;i++){
                              long startTime = System.currentTimeMillis();
                              a.clear2();
                              addStudent(a);
                              long endTime = System.currentTimeMillis();
                              sum1+=endTime-startTime;
                              }
                              //sum1/=500;
                              System.out.println("clear2: "+sum1);
                              -

                              Or to implement a multi-value map, Map<K,Collection>, supporting multiple values per key:

                              -
                              map.computeIfAbsent(key, k -> new HashSet<V>()).add(v);
                              -
                            18. -

                              它这说的还是很抽象的,下面给出一个使用computeIfAbsent的优雅实例:

                              -

                              TreeMap()) Treemap With Object

                              +

                              经测试发现不分伯仲()也确实差距应该很小2333

                              +

                              不过依照一个回答:

                              -

                              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.

                              +

                              Your approach explicitly throws the backing array away. The existing implementation attempts to reuse it. So even if your approach is faster in isolation, in practice it will almost certainly be less performant. Since calling clear() is a sign you intend to reuse the ArrayList.

                              -
                              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}}
                              +

                              其实感觉我的clear说不定花销更大,毕竟要创建一个新对象(

                              +
                            19. +
                            20. heap write traffic

                              What is heap write traffic and why it is required in ArrayList?

                              +

                              详见第二个答案。

                              + -

                              computIfAbsent发现此时map里面没有这个“line”key,就执行第二个参数的lambda表达式,把一个new TreeMap<>以line为关键字放入,并且返回该TreeMap。

                              +

                              栈放在一级缓存,堆放在二级缓存

                              +

                              总之意思就是成员变量写在堆里,局部变量写在栈里,做

                              +
                              while (i != size && modCount == expectedModCount) {
                              consumer.accept((E) elementData[i++]);
                              // Update cursor while iterating
                              cursor = i;
                              }
                              + +

                              比做

                              +
                              while (cursor != size && modCount == expectedModCount) {
                              consumer.accept((E) elementData[cursor++]);
                              }
                              + +

                              花销更小

                            21. -
                            22. 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)
                              +
                            23. if (i >= elementData.length)

                              is i >= elementData.length in ArrayList::iterator redundant?

                              +
                              public E previous() {
                              checkForComodification();
                              int i = cursor - 1;
                              if (i < 0)
                              throw new NoSuchElementException();
                              Object[] elementData = ArrayList.this.elementData;
                              if (i >= elementData.length)
                              throw new ConcurrentModificationException();
                              cursor = i;
                              return (E) elementData[lastRet = i];
                              }
                              -

                              所举代码段意为把新值通过字符串拼接接在旧值后面。

                              -

                              应该也可以用于集合合并。总之具体实现方法取决于传入的function参数,非常实用

                              -
                            24. +

                              如果 user invoke trimToSize method ,就会导致在checkForComodification();if (i >= elementData.length)之间发生ArrayIndexOutOfBounds。而在if (i >= elementData.length)之后trim没有影响,因为我们的局部变量已经保存了原来的elementData,此时再trim只是修改成员变量的elementData,局部变量依然不变。

                            25. -
                            26. 迭代器

                              map本身没有迭代器。

                              -

                              因而在对map进行遍历时,只能通过其keyset、valueset以及entryset来实现。

                              -

                              具体详见:HashMap的四种遍历方式

                              +
                            27. sublist不可序列化,且not cloneable
                              private class SubList extends AbstractList<E> implements RandomAccess 
                              + +

                              sublist没有extends Cloneable, java.io.Serializable这两个接口

                              +
                            28. +
                            29. parent和ArrayList.this

                              首先,这两个是同一个吗?其次,这俩是否是同一个跟sub的级数有关系吗,就比如一级sub都是同一个,多级sub就不是同一个了?

                              +

                              经过对ArrayList的一些public和以下代码的测试,得出结论:这两个只有在第一级sub的时候是同一个。parent指向直系父亲,ArrayList.this指向root父亲。

                              +
                              //In ArrayList:
                              public final AbstractList<E> parent;
                              public ArrayList getRoot(){return ArrayList.this;}
                              //In test main:
                              public static void main(String[] args) {
                              ArrayList a=new ArrayList();
                              addStudent(a);
                              System.out.println(a.hashCode());

                              ArrayList.SubList suba= (ArrayList.SubList) a.subList(1,7);
                              System.out.println(suba.getRoot().hashCode()+"\t"+suba.parent.hashCode());
                              System.out.println(suba.hashCode());

                              ArrayList.SubList subsuba=(ArrayList.SubList) suba.subList(2,4);
                              System.out.println(subsuba.getRoot().hashCode()+"\t"+subsuba.parent.hashCode());
                              System.out.println(subsuba.hashCode());

                              ArrayList.SubList subsubsuba=(ArrayList.SubList) subsuba.subList(0,1);
                              System.out.println(subsubsuba.getRoot().hashCode()+"\t"+subsubsuba.parent.hashCode());
                              }

                              /*输出结果:
                              779301330
                              779301330 779301330
                              -954172011
                              779301330 -954172011
                              -95519366
                              779301330 -95519366
                              */
                              + +

                              总之,ArrayList的sublist实现方式相当于串成了一条父子继承串,多级sub,至于这么干相比原来的只有两级父子关系的方法好在哪就不知道了

                            -

                            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.

                            +

                            AbstractSequentialList(A)

                            +

                            提供顺序访问list的基本骨架

                            +

                            To implement a list the programmer needs only to extend this class and provide implementations for the listIterator and size methods. For an unmodifiable list, the programmer need only implement the list iterator’s hasNext, next, hasPrevious, previous and index methods.
                            For a modifiable list the programmer should additionally implement the list iterator’s set method. For a variable-size list the programmer should additionally implement the list iterator’s remove and add methods.

                            - - -

                            最核心的还是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;
                            }
                            }
                            }
                            +

                            代码:

                            public abstract class AbstractSequentialList<E> extends AbstractList<E> {

                            protected AbstractSequentialList() {
                            }

                            public E get(int index) {
                            try {
                            //1
                            return listIterator(index).next();
                            } catch (NoSuchElementException exc) {
                            throw new IndexOutOfBoundsException("Index: "+index);
                            }
                            }

                            public E set(int index, E element) {
                            try {
                            ListIterator<E> e = listIterator(index);
                            E oldVal = e.next();
                            e.set(element);
                            return oldVal;
                            } catch (NoSuchElementException exc) {
                            throw new IndexOutOfBoundsException("Index: "+index);
                            }
                            }

                            public void add(int index, E element) {
                            try {
                            listIterator(index).add(element);
                            } catch (NoSuchElementException exc) {
                            throw new IndexOutOfBoundsException("Index: "+index);
                            }
                            }

                            public E remove(int index) {
                            try {
                            ListIterator<E> e = listIterator(index);
                            E outCast = e.next();
                            e.remove();
                            return outCast;
                            } catch (NoSuchElementException exc) {
                            throw new IndexOutOfBoundsException("Index: "+index);
                            }
                            }
                            // Bulk Operations

                            public boolean addAll(int index, Collection<? extends E> c) {
                            try {
                            boolean modified = false;
                            ListIterator<E> e1 = listIterator(index);
                            Iterator<? extends E> e2 = c.iterator();
                            while (e2.hasNext()) {
                            e1.add(e2.next());
                            modified = true;
                            }
                            return modified;
                            } catch (NoSuchElementException exc) {
                            throw new IndexOutOfBoundsException("Index: "+index);
                            }
                            }
                            // Iterators

                            public Iterator<E> iterator() {
                            return listIterator();
                            }

                            public abstract ListIterator<E> listIterator(int index);
                            }
                            -

                            其中:

                              -
                            1. 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不需要这个性质,因此作为成员变量花费更小

                              +

                              其中:

                                +
                              1. get和set方法通过Iterator实现

                                随机访问的AbstractList的iterator的方法借助了主类的get和set,跟这里正好反过来。但注意哈,下面的LinkedList实现把以上差不多所有的方法都重写了,因而get和set之类的方法,LinkedList并不是依靠迭代器的。

                              -

                              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等会看看

                              +

                              LinkedList

                              +

                              双向链表,实现List和Deque

                              +

                              并发不安全。List list = Collections.synchronizedList(new LinkedList(…));

                              +

                              印象:漂亮的指针操作,以及好像很少抛出异常,还有很多很多繁琐的方法(

                              -

                              总之意思差不多就是,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> {...}
                              }
                              +

                              代码:

                              public class LinkedList<E>
                              extends AbstractSequentialList<E>
                              implements List<E>, Deque<E>, Cloneable, java.io.Serializable
                              {
                              //怎么连size也是transient?这不是代表着该对象的信息吗(
                              transient int size = 0;

                              transient Node<E> first;

                              transient Node<E> last;

                              public LinkedList() {
                              }

                              public LinkedList(Collection<? extends E> c) {
                              //调用空构造器
                              this();
                              addAll(c);
                              }

                              //把e接在链表头
                              private void linkFirst(E e) {
                              final Node<E> f = first;
                              final Node<E> newNode = new Node<>(null, e, f);
                              first = newNode;
                              if (f == null)
                              last = newNode;
                              else
                              f.prev = newNode;
                              size++;
                              modCount++;
                              }

                              void linkLast(E e) {
                              final Node<E> l = last;
                              final Node<E> newNode = new Node<>(l, e, null);
                              last = newNode;
                              if (l == null)
                              first = newNode;
                              else
                              l.next = newNode;
                              size++;
                              modCount++;
                              }

                              // Inserts element e before non-null Node succ.
                              void linkBefore(E e, Node<E> succ) {
                              // assert succ != null;
                              final Node<E> pred = succ.prev;
                              final Node<E> newNode = new Node<>(pred, e, succ);
                              succ.prev = newNode;
                              if (pred == null)
                              first = newNode;
                              else
                              pred.next = newNode;
                              size++;
                              modCount++;
                              }

                              private E unlinkFirst(Node<E> f) {
                              // assert f == first && f != null;
                              //这里就只靠注释会不会危险了()不过也确实会自动帮我们抛出NullPointerException的
                              //而且我在想,不是first已经指向头结点了吗,那你为什么还要把头结点作为参数传进来。。。
                              final E element = f.item;
                              final Node<E> next = f.next;
                              f.item = null;
                              f.next = null; // help GC
                              first = next;
                              if (next == null)
                              last = null;
                              else
                              next.prev = null;
                              size--;
                              modCount++;
                              return element;
                              }

                              //3
                              private E unlinkLast(Node<E> l) {
                              // assert l == last && l != null;
                              final E element = l.item;
                              final Node<E> prev = l.prev;
                              l.item = null;
                              l.prev = null; // help GC
                              last = prev;
                              if (prev == null)
                              first = null;
                              else
                              prev.next = null;
                              size--;
                              modCount++;
                              return element;
                              }

                              E unlink(Node<E> x) {
                              // assert x != null;
                              final E element = x.item;
                              final Node<E> next = x.next;
                              final Node<E> prev = x.prev;

                              if (prev == null) {
                              first = next;
                              } else {
                              prev.next = next;
                              x.prev = null;
                              }

                              if (next == null) {
                              last = prev;
                              } else {
                              next.prev = prev;
                              x.next = null;
                              }

                              x.item = null;
                              size--;
                              modCount++;
                              return element;
                              }

                              public E getFirst() {
                              final Node<E> f = first;
                              if (f == null)
                              throw new NoSuchElementException();
                              return f.item;
                              }

                              public E getLast() {
                              final Node<E> l = last;
                              if (l == null)
                              throw new NoSuchElementException();
                              return l.item;
                              }

                              public E removeFirst() {
                              final Node<E> f = first;
                              if (f == null)
                              throw new NoSuchElementException();
                              return unlinkFirst(f);
                              }

                              public E removeLast() {
                              final Node<E> l = last;
                              if (l == null)
                              throw new NoSuchElementException();
                              return unlinkLast(l);
                              }

                              public void addFirst(E e) {linkFirst(e);}

                              public void addLast(E e) {linkLast(e);}

                              public boolean contains(Object o) {return indexOf(o) != -1;}

                              public int size() {return size;}

                              public boolean add(E e) {
                              linkLast(e);
                              return true;
                              }

                              public boolean remove(Object o) {
                              if (o == null) {
                              for (Node<E> x = first; x != null; x = x.next) {
                              if (x.item == null) {
                              unlink(x);
                              return true;
                              }
                              }
                              } else {
                              for (Node<E> x = first; x != null; x = x.next) {
                              if (o.equals(x.item)) {
                              unlink(x);
                              return true;
                              }
                              }
                              }
                              return false;
                              }

                              public boolean addAll(Collection<? extends E> c) {
                              return addAll(size, c);
                              }

                              public boolean addAll(int index, Collection<? extends E> c) {
                              checkPositionIndex(index);

                              Object[] a = c.toArray();
                              int numNew = a.length;
                              if (numNew == 0)
                              return false;

                              //分成两段
                              Node<E> pred, succ;
                              if (index == size) {
                              succ = null;
                              pred = last;
                              } else {
                              succ = node(index);
                              pred = succ.prev;
                              }

                              //往中间加料
                              for (Object o : a) {
                              @SuppressWarnings("unchecked") E e = (E) o;
                              Node<E> newNode = new Node<>(pred, e, null);
                              if (pred == null)
                              first = newNode;
                              else
                              pred.next = newNode;
                              pred = newNode;
                              }

                              //合起来
                              if (succ == null) {
                              last = pred;
                              } else {
                              pred.next = succ;
                              succ.prev = pred;
                              }

                              size += numNew;
                              modCount++;
                              return true;
                              }


                              public void clear() {
                              // 1
                              // Clearing all of the links between nodes is "unnecessary", but:
                              // - helps a generational GC if the discarded nodes inhabit
                              // more than one generation
                              // - is sure to free memory even if there is a reachable Iterator
                              for (Node<E> x = first; x != null; ) {
                              Node<E> next = x.next;
                              x.item = null;
                              x.next = null;
                              x.prev = null;
                              x = next;
                              }
                              first = last = null;
                              size = 0;
                              modCount++;
                              }

                              // Positional Access Operations

                              public E get(int index) {
                              checkElementIndex(index);
                              return node(index).item;
                              }

                              public E set(int index, E element) {
                              checkElementIndex(index);
                              Node<E> x = node(index);
                              E oldVal = x.item;
                              x.item = element;
                              return oldVal;
                              }

                              public void add(int index, E element) {
                              checkPositionIndex(index);

                              if (index == size)
                              linkLast(element);
                              else
                              linkBefore(element, node(index));
                              }

                              public E remove(int index) {
                              checkElementIndex(index);
                              return unlink(node(index));
                              }

                              Node<E> node(int index) {
                              // assert isElementIndex(index);
                              //所以为啥不能check一下?

                              //做了个小小的优化,如果在前半就从开头找,在后半就从最后往前找
                              if (index < (size >> 1)) {
                              Node<E> x = first;
                              for (int i = 0; i < index; i++)
                              x = x.next;
                              return x;
                              } else {
                              Node<E> x = last;
                              for (int i = size - 1; i > index; i--)
                              x = x.prev;
                              return x;
                              }
                              }

                              // Search Operations

                              public int indexOf(Object o) {...}

                              public int lastIndexOf(Object o) {...}

                              // Queue operations.
                              //部分省略

                              //always return true
                              public boolean offer(E e) {
                              return add(e);
                              }

                              // Deque operations
                              //省略

                              public boolean removeFirstOccurrence(Object o) {
                              //666
                              return remove(o);
                              }

                              public boolean removeLastOccurrence(Object o) {...}

                              public ListIterator<E> listIterator(int index) {
                              checkPositionIndex(index);
                              return new ListItr(index);
                              }

                              //非常聪明非常漂亮的指针操作
                              private class ListItr implements ListIterator<E> {
                              private Node<E> lastReturned;
                              private Node<E> next;
                              private int nextIndex;
                              private int expectedModCount = modCount;

                              ListItr(int index) {
                              // assert isPositionIndex(index);
                              next = (index == size) ? null : node(index);
                              nextIndex = index;
                              }

                              public boolean hasNext() {
                              return nextIndex < size;
                              }

                              public E next() {
                              checkForComodification();
                              if (!hasNext())
                              throw new NoSuchElementException();

                              lastReturned = next;
                              next = next.next;
                              nextIndex++;
                              return lastReturned.item;
                              }

                              public boolean hasPrevious() {
                              return nextIndex > 0;
                              }

                              public E previous() {
                              checkForComodification();
                              if (!hasPrevious())
                              throw new NoSuchElementException();

                              //注意这个next是内部类里的成员变量,last是外部类的成员变量
                              lastReturned = next = (next == null) ? last : next.prev;
                              nextIndex--;
                              return lastReturned.item;
                              }

                              public int nextIndex() {
                              return nextIndex;
                              }

                              public int previousIndex() {
                              return nextIndex - 1;
                              }

                              public void remove() {
                              checkForComodification();
                              if (lastReturned == null)
                              throw new IllegalStateException();

                              Node<E> lastNext = lastReturned.next;
                              unlink(lastReturned);
                              if (next == lastReturned)
                              next = lastNext;
                              else
                              nextIndex--;
                              lastReturned = null;
                              expectedModCount++;
                              }

                              public void set(E e) {
                              if (lastReturned == null)
                              throw new IllegalStateException();
                              checkForComodification();
                              lastReturned.item = e;
                              }

                              public void add(E e) {
                              checkForComodification();
                              lastReturned = null;
                              if (next == null)
                              linkLast(e);
                              else
                              linkBefore(e, next);
                              nextIndex++;
                              expectedModCount++;
                              }

                              public void forEachRemaining(Consumer<? super E> action) {
                              Objects.requireNonNull(action);
                              while (modCount == expectedModCount && nextIndex < size) {
                              action.accept(next.item);
                              lastReturned = next;
                              next = next.next;
                              nextIndex++;
                              }
                              checkForComodification();
                              }

                              final void checkForComodification() {
                              if (modCount != expectedModCount)
                              throw new ConcurrentModificationException();
                              }
                              }

                              //节点类,平平无奇链表捏
                              private static class Node<E> {
                              E item;
                              Node<E> next;
                              Node<E> prev;

                              Node(Node<E> prev, E element, Node<E> next) {
                              this.item = element;
                              this.next = next;
                              this.prev = prev;
                              }
                              }

                              public Iterator<E> descendingIterator() {
                              return new DescendingIterator();
                              }

                              // 2 降序迭代器
                              private class DescendingIterator implements Iterator<E> {
                              //借助升序迭代器实现
                              private final ListItr itr = new ListItr(size());

                              public boolean hasNext() {
                              return itr.hasPrevious();
                              }
                              public E next() {
                              return itr.previous();
                              }
                              public void remove() {
                              itr.remove();
                              }
                              }

                              @SuppressWarnings("unchecked")
                              private LinkedList<E> superClone() {
                              try {
                              return (LinkedList<E>) super.clone();
                              } catch (CloneNotSupportedException e) {
                              throw new InternalError(e);
                              }
                              }

                              public Object clone() {
                              LinkedList<E> clone = superClone();

                              // 初始化
                              clone.first = clone.last = null;
                              clone.size = 0;
                              clone.modCount = 0;

                              // Initialize clone with our elements
                              for (Node<E> x = first; x != null; x = x.next)
                              clone.add(x.item);

                              return clone;
                              }

                              public Object[] toArray() {...}

                              @SuppressWarnings("unchecked")
                              public <T> T[] toArray(T[] a) {...}

                              private static final long serialVersionUID = 876323262645176354L;

                              private void writeObject(java.io.ObjectOutputStream s)
                              throws java.io.IOException {...}

                              @SuppressWarnings("unchecked")
                              private void readObject(java.io.ObjectInputStream s)
                              throws java.io.IOException, ClassNotFoundException {...}

                              @Override
                              public Spliterator<E> spliterator() {
                              return new LLSpliterator<E>(this, -1, 0);
                              }

                              static final class LLSpliterator<E> implements Spliterator<E> {...}
                              //4
                              }
                              -

                              其中:

                                -
                              1. hash()

                                +

                                其中

                                  +
                                1. 关于分代GC

                                  In the clear() function:

                                  +
                                  // Clearing all of the links between nodes is "unnecessary", but:
                                  // - helps a generational GC if the discarded nodes inhabit
                                  // more than one generation
                                  // - is sure to free memory even if there is a reachable Iterator
                                  -

                                  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. 降序迭代器

                                  一切都反过来了,也没有升序迭代器恁多方法:

                                  +

                                  不支持foreach循环,只支持单向遍历,没有add set 只有remove。

                                5. -
                                6. 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。

                                  +
                                7. 关于unlinkLast/First的参数问题

                                  In Java LinkedList source code, why the unlinkFirst function should have a param pointing to the first node?

                                  +

                                  事实证明确实人家也觉得无参比较合理(

                                8. -
                                9. entrySet

                                  不同于AbstractMap中entrySet的核心作用,HashMap的put、get、clear等等等核心函数都不依赖于entrySet了,毕竟结构改变得比较多了。因而这里的entrySet字段保留,只是为了呼应AbstractMap中keyset和valueset的实现,以及补充AbstractMap中未给出的EntrySet实现。

                                  +
                                10. sublist

                                  LinkedList用了从AbstractList继承来的sublist相关类和方法,没有特别的优化,其sublist不可序列化,且not cloneable。

                                11. -
                                12. 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;
                                13. -
                                14. 红黑树

                                  红黑树快速入门

                                  -

                                  这篇文章也写得很好:

                                  -

                                  算法:基于红黑树的 TreeMap

                                  +
                                15. transient的三个成员变量

                                  In Java LinkedList, why the “first”, “last” and “size” variable are transient?

                                  +
                                  +

                                  LinkedList provide its own method for serializing and de-serializing.

                                  +

                                  When serializing, it only writes the size, and the values of the list.

                                  +

                                  When deserializing, it reads the size, then build the list from scratch, each node for each value at a time.

                                  +

                                  If the author did not provide their own read and write methods, then they would need to make size, first, and last non-transient. They would also need to make the Node class serializable.

                                  +
                                  +

                                  As all the member variables of java LinkedList is transient, what will be the use of implementing Serializable?

                                  +
                                  +

                                  Otherwise serializing would be by default, which would be recursive, and for a large list would easily blow the stack.

                                  +
                                  +

                                  意思就是序列化时不记录这些信息,反序列化时会重新构建。还有说如果用默认的序列化方法是递归的可能爆栈?还有我觉得有一点可能是如果把所有node都序列化了,可能反序列化后,本来分配到那段内存空间要是被占用了,但指针值不变还是会有问题?等待之后解答。

                                16. -
                                17. HashIterator

                                  注意点有二:

                                  -

                                  ①不继承Iterator接口

                                  -

                                  ②抽象,具体实现类为EntryIterator、KeyIterator和ValueIterator

                                  -

                                  ③map的接口定义是没有iterator的,因此map不能通过hashiterator迭代,只能通过其vie来实现【三个具体实现类】

                                  +
                                +

                                Vector

                                +

                                Unlike the new collection implementations, Vector is synchronized. If a thread-safe implementation is not needed, it is recommended to use ArrayList in place of Vector.

                                +
                                +

                                代码:

                                public class Vector<E>
                                extends AbstractList<E>
                                implements List<E>, RandomAccess, Cloneable, java.io.Serializable
                                {

                                protected Object[] elementData;

                                protected int elementCount;

                                protected int capacityIncrement;

                                private static final long serialVersionUID = -2767605614048989439L;

                                public Vector(int initialCapacity, int capacityIncrement) {
                                super();
                                if (initialCapacity < 0)
                                throw new IllegalArgumentException("Illegal Capacity: "+
                                initialCapacity);
                                this.elementData = new Object[initialCapacity];
                                this.capacityIncrement = capacityIncrement;
                                }

                                public Vector(int initialCapacity) {
                                this(initialCapacity, 0);
                                }

                                public Vector() {
                                //1
                                this(10);
                                }

                                public Vector(Collection<? extends E> c) {
                                Object[] a = c.toArray();
                                elementCount = a.length;
                                if (c.getClass() == ArrayList.class) {
                                elementData = a;
                                } else {
                                elementData = Arrays.copyOf(a, elementCount, Object[].class);
                                }
                                }

                                public synchronized void copyInto(Object[] anArray) {
                                System.arraycopy(elementData, 0, anArray, 0, elementCount);
                                }

                                //2
                                public synchronized void trimToSize() {
                                modCount++;
                                int oldCapacity = elementData.length;
                                if (elementCount < oldCapacity) {
                                elementData = Arrays.copyOf(elementData, elementCount);
                                }
                                }

                                public synchronized void ensureCapacity(int minCapacity) {
                                if (minCapacity > 0) {
                                modCount++;
                                ensureCapacityHelper(minCapacity);
                                }
                                }

                                private void ensureCapacityHelper(int minCapacity) {
                                // overflow-conscious code
                                if (minCapacity - elementData.length > 0)
                                grow(minCapacity);
                                }

                                private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

                                private void grow(int minCapacity) {...}

                                private static int hugeCapacity(int minCapacity) {...}

                                /*
                                Sets the size of this vector. If the new size is greater than the current size, new null items are added to the end of the vector. If the new size is less than the current size, all components at index newSize and greater are discarded.
                                */
                                public synchronized void setSize(int newSize) {
                                modCount++;
                                if (newSize > elementCount) {
                                ensureCapacityHelper(newSize);
                                } else {
                                for (int i = newSize ; i < elementCount ; i++)
                                //3 GC帮大忙,注意这里没有trim。
                                elementData[i] = null;
                                }
                                }
                                elementCount = newSize;
                                }

                                //capacity 、size 、isEmpty省略

                                //4
                                public Enumeration<E> elements() {
                                return new Enumeration<E>() {
                                int count = 0;
                                public boolean hasMoreElements() {
                                return count < elementCount;
                                }
                                public E nextElement() {
                                synchronized (Vector.this) {
                                if (count < elementCount) {
                                return elementData(count++);
                                }
                                }
                                throw new NoSuchElementException("Vector Enumeration");
                                }
                                };
                                }

                                //contains、indexof、lastIndexOf省略

                                public synchronized E elementAt(int index) {
                                if (index >= elementCount) {
                                throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
                                }

                                return elementData(index);
                                }

                                public synchronized E firstElement() {
                                if (elementCount == 0) {
                                throw new NoSuchElementException();
                                }
                                return elementData(0);
                                }

                                public synchronized E lastElement() {
                                if (elementCount == 0) {
                                throw new NoSuchElementException();
                                }
                                return elementData(elementCount - 1);
                                }

                                public synchronized void setElementAt(E obj, int index) {...}

                                public synchronized void removeElementAt(int index) {...}

                                public synchronized void insertElementAt(E obj, int index) {...}

                                public synchronized void addElement(E obj) {...}

                                public synchronized boolean removeElement(Object obj) {...}

                                //clear
                                public synchronized void removeAllElements() {
                                modCount++;
                                // Let gc do its work
                                for (int i = 0; i < elementCount; i++)
                                elementData[i] = null;

                                elementCount = 0;
                                }

                                public synchronized Object clone() {...}

                                public synchronized Object[] toArray() {
                                return Arrays.copyOf(elementData, elementCount);
                                }

                                @SuppressWarnings("unchecked")
                                public synchronized <T> T[] toArray(T[] a) {...}

                                // Positional Access Operations
                                //add set get remove clear省略

                                // Bulk Operations
                                // xxxAll省略

                                //用了AbstractList的equal、hashcode、tostring,省略

                                public synchronized List<E> subList(int fromIndex, int toIndex) {
                                return Collections.synchronizedList(super.subList(fromIndex, toIndex),
                                this);
                                }

                                protected synchronized void removeRange(int fromIndex, int toIndex) {...}

                                //跟AL不一样
                                private void readObject(ObjectInputStream in)
                                throws IOException, ClassNotFoundException {
                                ObjectInputStream.GetField gfields = in.readFields();
                                int count = gfields.get("elementCount", 0);
                                Object[] data = (Object[])gfields.get("elementData", null);
                                if (count < 0 || data == null || count > data.length) {
                                throw new StreamCorruptedException("Inconsistent vector internals");
                                }
                                elementCount = count;
                                elementData = data.clone();
                                }

                                private void writeObject(java.io.ObjectOutputStream s)
                                throws java.io.IOException {
                                final java.io.ObjectOutputStream.PutField fields = s.putFields();
                                final Object[] data;
                                synchronized (this) {
                                fields.put("capacityIncrement", capacityIncrement);
                                fields.put("elementCount", elementCount);
                                data = elementData.clone();
                                }
                                fields.put("elementData", data);
                                s.writeFields();
                                }

                                public synchronized ListIterator<E> listIterator(int index) {
                                if (index < 0 || index > elementCount)
                                throw new IndexOutOfBoundsException("Index: "+index);
                                return new ListItr(index);
                                }

                                public synchronized ListIterator<E> listIterator() {
                                return new ListItr(0);
                                }

                                public synchronized Iterator<E> iterator() {
                                return new Itr();
                                }

                                private class Itr implements Iterator<E> {...}

                                final class ListItr extends Itr implements ListIterator<E> {...}

                                @Override
                                public synchronized void forEach(Consumer<? super E> action) {...}

                                @Override
                                @SuppressWarnings("unchecked")
                                public synchronized boolean removeIf(Predicate<? super E> filter) {...}

                                @Override
                                @SuppressWarnings("unchecked")
                                public synchronized void replaceAll(UnaryOperator<E> operator) {...}

                                @SuppressWarnings("unchecked")
                                @Override
                                public synchronized void sort(Comparator<? super E> c) {...}

                                @Override
                                public Spliterator<E> spliterator() {
                                return new VectorSpliterator<>(this, null, 0, -1, 0);
                                }

                                static final class VectorSpliterator<E> implements Spliterator<E> {...}
                                }
                                + +

                                其中:

                                  +
                                1. 默认容量

                                  空构造器Vector()创建出来的默认容量为10,不同于ArrayList是个空集合。

                                  +
                                2. +
                                3. 扩容操作

                                  与ArrayList基本上是雷同的,就是都是synchronized。

                                4. +
                                5. setsize不改变容量

                                  实现中只是把东西设置为空,并没有trim,因而容量不变

                                  +
                                6. +
                                7. Vector可生成枚举类
                                -

                                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.

                                - +

                                Set

                                Set(I)

                                代码:

                                /* 1
                                Note: 当set里包含可变的对象时,要多加小心。
                                The behavior of a set is not specified if the value of a set object is changed in a manner that affects equals comparisons.
                                A special case of this prohibition is that it is not permissible for a set to contain itself as an element.
                                */
                                public interface Set<E> extends Collection<E> {
                                // Query Operations
                                int size();

                                boolean isEmpty();

                                boolean contains(Object o);
                                /*Returns an iterator over the elements in this set.
                                The elements are returned in no particular order
                                (unless this set is an instance of some class that provides a guarantee).*/
                                Iterator<E> iterator();

                                Object[] toArray();

                                <T> T[] toArray(T[] a);

                                // Modification Operations

                                /*
                                If this set already contains the element,
                                the call leaves the set unchanged and returns false.
                                */
                                boolean add(E e);

                                boolean remove(Object o);

                                // Bulk Operations

                                /*
                                Returns true if this set contains all of the elements of the specified collection.
                                If the specified collection is also a set,
                                this method returns true if it is a subset of this set.[这个subset的定义有点意思]
                                */
                                boolean containsAll(Collection<?> c);

                                //2
                                //取并集
                                boolean addAll(Collection<? extends E> c);

                                //取交集
                                boolean retainAll(Collection<?> c);

                                //集合差
                                boolean removeAll(Collection<?> c);

                                void clear();

                                // Comparison and hashing

                                boolean equals(Object o);

                                int hashCode();

                                @Override
                                default Spliterator<E> spliterator() {
                                return Spliterators.spliterator(this, Spliterator.DISTINCT);
                                }
                                }
                                -

                                这样可以保持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”怎么定义:

                                +

                                其中:

                                  +
                                1. set中包含可变对象

                                  需要规避可修改对象,使其与集合中另一个元素重复的问题

                                  +

                                  详见此:

                                  +

                                  Java HashSet contains duplicates if contained element is modified

                                  -

                                  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

                                  +

                                  The correct solution is to stick to the contract of Set and not modify objects after adding them to the collection.

                                  +

                                  You can avoid this problem by either:

                                  +
                                    +
                                  • using an immutable type for your set elements,
                                  • +
                                  • making a copy of the objects as you put them into the set and / or pull them out of the set,
                                  • +
                                  • writing your code so that it “knows“ not to change the objects for the duration …
                                  • +
                                  +

                                  From the perspective of correctness and robustness, the first option is clearly best.

                                  -

                                  也因此,对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)
                                  */
                                  +
                                2. +
                                3. 集合操作

                                  set抽象自数学的集合,因此有很多对应的集合操作:

                                  +
                                  +

                                  addAll ∪

                                  +

                                  retainAll ∩

                                  +

                                  removeAll -

                                  -

                                  总之意思就是,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也可以它却重写了。原来是因为这个性能问题啊

                                  -

                                  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

                                  +

                                  AbstratcSet(A)

                                  +

                                  Note that this class does not override any of the implementations from the AbstractCollection class. It merely adds implementations for equals and hashCode.

                                  -

                                  最大的特点就是可以人为定义有序并且有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();
                                  }
                                  +

                                  代码:

                                  public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E> {

                                  protected AbstractSet() {
                                  }

                                  // Comparison and hashing

                                  public boolean equals(Object o) {
                                  if (o == this)
                                  return true;

                                  if (!(o instanceof Set))
                                  return false;
                                  Collection<?> c = (Collection<?>) o;
                                  if (c.size() != size())
                                  return false;
                                  try {
                                  return containsAll(c);
                                  } catch (ClassCastException unused) {
                                  return false;
                                  } catch (NullPointerException unused) {
                                  return false;
                                  }
                                  }

                                  public int hashCode() {
                                  int h = 0;
                                  Iterator<E> i = iterator();
                                  while (i.hasNext()) {
                                  E obj = i.next();
                                  if (obj != null)
                                  h += obj.hashCode();
                                  }
                                  return h;
                                  }

                                  //不大明白为啥要修改实现,用AbstractCollection的不好吗
                                  public boolean removeAll(Collection<?> c) {
                                  Objects.requireNonNull(c);
                                  boolean modified = false;

                                  if (size() > c.size()) {
                                  for (Iterator<?> i = c.iterator(); i.hasNext(); )
                                  modified |= remove(i.next());
                                  } else {
                                  for (Iterator<?> i = iterator(); i.hasNext(); ) {
                                  if (c.contains(i.next())) {
                                  i.remove();
                                  modified = true;
                                  }
                                  }
                                  }
                                  return modified;
                                  }
                                  }
                                  -
                                  -

                                  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的快照

                                  +

                                  HashSet

                                  +

                                  In particular, it does not guarantee that the order will remain constant over time.

                                  +

                                  Iterating over this set requires time proportional to the sum of the HashSet instance’s size (the number of elements) plus the “capacity” of the backing HashMap instance (the number of buckets).

                                  +

                                  Thus, it’s very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important.

                                  +

                                  This implementation is not synchronized. Set s = Collections.synchronizedSet(new HashSet(...));

                                  -

                                  跟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);
                                  }
                                  +

                                  代码:

                                  public class HashSet<E>
                                  extends AbstractSet<E>
                                  implements Set<E>, Cloneable, java.io.Serializable
                                  {
                                  static final long serialVersionUID = -5024744406713321676L;

                                  //通过hashmap实现
                                  //map不可序列化
                                  private transient HashMap<E,Object> map;

                                  // Dummy value to associate with an Object in the backing Map
                                  // 1
                                  private static final Object PRESENT = new Object();

                                  //Constructs a new, empty set;
                                  //the backing HashMap instance has default initial capacity (16)
                                  //and load factor (0.75).
                                  public HashSet() {
                                  map = new HashMap<>();
                                  }

                                  //The HashMap is created with default load factor (0.75)
                                  //and an initial capacity sufficient to contain the elements in c
                                  public HashSet(Collection<? extends E> c) {
                                  //看来这个load factor=size/0.75
                                  map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
                                  addAll(c);
                                  }

                                  public HashSet(int initialCapacity, float loadFactor) {
                                  map = new HashMap<>(initialCapacity, loadFactor);
                                  }

                                  public HashSet(int initialCapacity) {
                                  map = new HashMap<>(initialCapacity);
                                  }

                                  //This package private constructor is only used by LinkedHashSet.
                                  //@param: dummy – ignored (distinguishes this constructor from other constructor.)
                                  HashSet(int initialCapacity, float loadFactor, boolean dummy) {
                                  //此处为LinkeHashMap
                                  map = new LinkedHashMap<>(initialCapacity, loadFactor);
                                  }

                                  public Iterator<E> iterator() {return map.keySet().iterator();}

                                  public int size() {return map.size();}

                                  public boolean isEmpty() {return map.isEmpty();}

                                  public boolean contains(Object o) {return map.containsKey(o);}

                                  //@return true if this set did not already contain the specified element
                                  //map.put返回已有的oldValue,返回空表示没有oldValue,插入成功;否则失败
                                  public boolean add(E e) {
                                  return map.put(e, PRESENT)==null;
                                  }

                                  public boolean remove(Object o) {
                                  return map.remove(o)==PRESENT;
                                  }

                                  public void clear() {
                                  map.clear();
                                  }

                                  @SuppressWarnings("unchecked")
                                  public Object clone() {
                                  try {
                                  HashSet<E> newSet = (HashSet<E>) super.clone();
                                  newSet.map = (HashMap<E, Object>) map.clone();
                                  return newSet;
                                  } catch (CloneNotSupportedException e) {
                                  throw new InternalError(e);
                                  }
                                  }

                                  private void writeObject(java.io.ObjectOutputStream s)
                                  throws java.io.IOException {...}

                                  private void readObject(java.io.ObjectInputStream s)
                                  throws java.io.IOException, ClassNotFoundException {...}

                                  public Spliterator<E> spliterator() {
                                  return new HashMap.KeySpliterator<E,Object>(map, 0, -1, 0, 0);
                                  }
                                  }
                                  -

                                  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.

                                    +

                                    其中:

                                      +
                                    1. PRESENT

                                      正如它的解释:

                                      +
                                      +

                                      Dummy value to associate with an Object in the backing Map

                                      -

                                      都使用了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有序【这俩靠红黑树的遍历顺序(二叉搜索树嘛)】。

                                      +

                                      set 以map作为内部支持,其实主要用的是map对于key的高效去重。也就是说,set其实只需要用map的key这一半就好了。所以我们另一半value就都统一用一个new Object【也就是PRESENT】来统一就行。

                                      +

                                      不得不说这点很聪明,值得学习。

                                      +
                                      private static final Object PRESENT = new Object();
                                      public boolean add(E e) {
                                      return map.put(e, PRESENT)==null;
                                      }
                                      public boolean remove(Object o) {
                                      return map.remove(o)==PRESENT;
                                      }
                                    8. +
                                    +

                                    SortedSet(I)

                                    +

                                    A Set that further provides a total ordering on its elements.

                                    +

                                    The set’s iterator will traverse the set in ascending element order.

                                    +

                                    All elements inserted into a sorted set must implement the Comparable interface (or be accepted by the specified comparator).否则导致ClassCastException

                                    +

                                    Note that the ordering maintained by a sorted set (whether or not an explicit comparator is provided) must be consistent with equals if the sorted set is to correctly implement the Set interface.

                                    +

                                    【consistent with equals:if and only if c.compare(e1, e2)==0 has the same boolean value as e1.equals(e2) for every e1 and e2 in S.】

                                    +

                                    //2

                                    +

                                    Note: several methods return subsets with restricted ranges. 区间是前闭后开的。

                                    +

                                    If you need a closed range , and the element type allows for calculation of the successor of a given value, merely request the subrange from lowEndpoint to successor(highEndpoint).

                                    +

                                    For example, suppose that s is a sorted set of strings. [low,hight]
                                    SortedSet<String> sub = s.subSet(low, high+"\0");
                                    A similar technique can be used to generate an open range (low,hight)
                                    SortedSet<String> sub = s.subSet(low+"\0", high);

                                    +

                                    66666

                                    +
                                    +

                                    代码

                                    public interface SortedSet<E> extends Set<E> {
                                    //null if this set uses the natural ordering
                                    Comparator<? super E> comparator();
                                    //1
                                    SortedSet<E> subSet(E fromElement, E toElement);

                                    SortedSet<E> headSet(E toElement);

                                    SortedSet<E> tailSet(E fromElement);

                                    E first();

                                    E last();

                                    @Override
                                    default Spliterator<E> spliterator() {
                                    return new Spliterators.IteratorSpliterator<E>(
                                    this, Spliterator.DISTINCT | Spliterator.SORTED | Spliterator.ORDERED) {
                                    @Override
                                    public Comparator<? super E> getComparator() {
                                    return SortedSet.this.comparator();
                                    }
                                    };
                                    }
                                    }
                                    + +

                                    其中:

                                      +
                                    1. subset

                                      只有sorted set才有subset,想想也确实

                                      +
                                      /*
                                      Throws:
                                      ClassCastException – if fromElement and toElement cannot be compared to one another using this set's comparator
                                      NullPointerException – if fromElement or toElement is null and this set does not permit null elements
                                      IllegalArgumentException – if fromElement is greater than toElement; or fromElement or toElement lies outside the bounds of the restricted range of the set
                                      */
                                      //The returned set will throw an IllegalArgumentException
                                      //on an attempt to insert an element outside its range.
                                      SortedSet<E> subSet(E fromElement, E toElement);
                                      + +

                                      以及注意此处是Element,不是Index

                                    2. -
                                    3. 实现的约定接口

                                      都Cloneable,Serializable

                                      -

                                      ArrayList/Vector:RandomAccess

                                      +
                                    4. subSet(low, high+”\0”);

                                      为什么加个”\0”就可以,具体可以看看这个:

                                      +

                                      Adding “\0” to a subset range end

                                      +

                                      原因就是sub的这个range取的是在此区间的元素,low和high这两个param不一定要包含在这个set里面。因此,按照set的排序,high+”\0”比high大,因而high就落入此区间,也就可以被包含在range中了。

                                    +
                                    +

                                    比起sorted set,navigable set最特殊的点在于它提供了对某一元素附近元素的导航。

                                    +

                                    Method usage

                                    +

                                    lower less than最大的,比所给ele小的元素

                                    +

                                    floor less than or equal最大的,比所给ele小或者等于的元素

                                    +

                                    ceiling greater than or equal最小的,比所给ele大或者等于的元素

                                    +

                                    higher greater than最小的,比所给ele大的元素

                                    +

                                    The descendingSet method returns a view of the set with the senses of all relational and directional methods inverted.

                                    +

                                    This interface additionally defines methods pollFirst and pollLast that return and remove the lowest and highest element, if one exists, else returning null. 有点堆的感觉

                                    +

                                    Methods subSet, headSet, and tailSet differ from the like-named SortedSet methods in accepting additional arguments describing whether lower and upper bounds are inclusive versus exclusive.

                                    +
                                    +

                                    代码:

                                    public interface NavigableSet<E> extends SortedSet<E> {
                                    //Returns the greatest element in this set strictly less than the given element
                                    E lower(E e);

                                    E floor(E e);

                                    E ceiling(E e);

                                    E higher(E e);

                                    //Removes the first (lowest) element, or returns null if this set is empty.
                                    E pollFirst();

                                    E pollLast();

                                    //in ascending order
                                    Iterator<E> iterator();

                                    /*
                                    The returned set has an ordering equivalent to
                                    Collections.reverseOrder(comparator()).
                                    The expression s.descendingSet().descendingSet()
                                    returns a view of s essentially equivalent(基本等价) to s
                                    */
                                    NavigableSet<E> descendingSet();

                                    //Equivalent in effect to descendingSet().iterator()
                                    Iterator<E> descendingIterator();

                                    NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
                                    E toElement, boolean toInclusive);

                                    NavigableSet<E> headSet(E toElement, boolean inclusive);

                                    NavigableSet<E> tailSet(E fromElement, boolean inclusive);

                                    SortedSet<E> subSet(E fromElement, E toElement);

                                    SortedSet<E> headSet(E toElement);

                                    SortedSet<E> tailSet(E fromElement);
                                    }
                                    + +

                                    TreeSet

                                    +

                                    This implementation provides guaranteed log(n) time cost for the basic operations (add, remove and contains).

                                    +

                                    Note that this implementation is not synchronized.

                                    +

                                    SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...));

                                    +
                                    +

                                    代码:

                                    public class TreeSet<E> extends AbstractSet<E>
                                    implements NavigableSet<E>, Cloneable, java.io.Serializable
                                    {

                                    //依然用了个map
                                    private transient NavigableMap<E,Object> m;

                                    // Dummy value to associate with an Object in the backing Map
                                    private static final Object PRESENT = new Object();

                                    TreeSet(NavigableMap<E,Object> m) {
                                    this.m = m;
                                    }

                                    public TreeSet() {
                                    this(new TreeMap<E,Object>());
                                    }

                                    public TreeSet(Comparator<? super E> comparator) {
                                    this(new TreeMap<>(comparator));
                                    }

                                    public TreeSet(Collection<? extends E> c) {
                                    this();
                                    addAll(c);
                                    }

                                    public TreeSet(SortedSet<E> s) {
                                    this(s.comparator());
                                    addAll(s);
                                    }

                                    public Iterator<E> iterator() {
                                    return m.navigableKeySet().iterator();
                                    }

                                    public Iterator<E> descendingIterator() {
                                    return m.descendingKeySet().iterator();
                                    }

                                    //确实直接让map倒序就可以了
                                    public NavigableSet<E> descendingSet() {
                                    return new TreeSet<>(m.descendingMap());
                                    }

                                    public int size() {
                                    return m.size();
                                    }

                                    public boolean isEmpty() {
                                    return m.isEmpty();
                                    }

                                    public boolean contains(Object o) {
                                    return m.containsKey(o);
                                    }

                                    public boolean add(E e) {
                                    return m.put(e, PRESENT)==null;
                                    }

                                    public boolean remove(Object o) {
                                    return m.remove(o)==PRESENT;
                                    }

                                    public void clear() {
                                    m.clear();
                                    }

                                    public boolean addAll(Collection<? extends E> c) {
                                    // Use linear-time version if applicable
                                    if (m.size()==0 && c.size() > 0 &&
                                    c instanceof SortedSet &&
                                    m instanceof TreeMap) {
                                    SortedSet<? extends E> set = (SortedSet<? extends E>) c;
                                    TreeMap<E,Object> map = (TreeMap<E, Object>) m;
                                    Comparator<?> cc = set.comparator();
                                    Comparator<? super E> mc = map.comparator();
                                    if (cc==mc || (cc != null && cc.equals(mc))) {
                                    map.addAllForTreeSet(set, PRESENT);
                                    return true;
                                    }
                                    }
                                    return super.addAll(c);
                                    }

                                    public NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
                                    E toElement, boolean toInclusive) {
                                    return new TreeSet<>(m.subMap(fromElement, fromInclusive,
                                    toElement, toInclusive));
                                    }

                                    public NavigableSet<E> headSet(E toElement, boolean inclusive) {
                                    return new TreeSet<>(m.headMap(toElement, inclusive));
                                    }

                                    public NavigableSet<E> tailSet(E fromElement, boolean inclusive) {
                                    return new TreeSet<>(m.tailMap(fromElement, inclusive));
                                    }

                                    public SortedSet<E> subSet(E fromElement, E toElement) {
                                    return subSet(fromElement, true, toElement, false);
                                    }

                                    public SortedSet<E> headSet(E toElement) {
                                    return headSet(toElement, false);
                                    }

                                    //注意此处为true。严格遵循左闭右开
                                    public SortedSet<E> tailSet(E fromElement) {
                                    return tailSet(fromElement, true);
                                    }

                                    public Comparator<? super E> comparator() {
                                    return m.comparator();
                                    }

                                    public E first() {
                                    return m.firstKey();
                                    }

                                    public E last() {
                                    return m.lastKey();
                                    }

                                    // NavigableSet API methods

                                    public E lower(E e) {
                                    return m.lowerKey(e);
                                    }

                                    public E floor(E e) {
                                    return m.floorKey(e);
                                    }

                                    public E ceiling(E e) {
                                    return m.ceilingKey(e);
                                    }

                                    public E higher(E e) {
                                    return m.higherKey(e);
                                    }

                                    public E pollFirst() {
                                    Map.Entry<E,?> e = m.pollFirstEntry();
                                    return (e == null) ? null : e.getKey();
                                    }

                                    public E pollLast() {
                                    Map.Entry<E,?> e = m.pollLastEntry();
                                    return (e == null) ? null : e.getKey();
                                    }

                                    @SuppressWarnings("unchecked")
                                    public Object clone() {
                                    TreeSet<E> clone;
                                    try {
                                    clone = (TreeSet<E>) super.clone();
                                    } catch (CloneNotSupportedException e) {
                                    throw new InternalError(e);
                                    }

                                    clone.m = new TreeMap<>(m);
                                    return clone;
                                    }

                                    private void writeObject(java.io.ObjectOutputStream s)
                                    throws java.io.IOException {...}

                                    private void readObject(java.io.ObjectInputStream s)
                                    throws java.io.IOException, ClassNotFoundException {...}

                                    public Spliterator<E> spliterator() {
                                    return TreeMap.keySpliteratorFor(m);
                                    }

                                    private static final long serialVersionUID = -2479143000061671589L;
                                    }
                                    + ]]> Java @@ -13756,553 +14001,311 @@ url访问填写http://localhost/webdemo4_war/*.do
                                  2. 以字符串操作为例,提供的标准C语言库。

                                  -

                                  之后,我们将其以如下参数编译为静态库:

                                  -
                                  $ gcc -c -fno-builtin -nostdlib -fno-stack-protector entry.c malloc.c stdio.c string.c printf.c
                                  $ ar -rs minicrt.a malloc.o printf.o stdio.o string.o
                                  # 编译测试用例
                                  $ gcc -m32 -c -ggdb -fno-builtin -nostdlib -fno-stack-protector test.c
                                  - -

                                  再指定mini_crt_entry为入口进行静态链接:

                                  -
                                  $ ld -m elf_i386 -static -e mini_crt_entry entry.o test.o minicrt.a -o test
                                4. -
                                5. C++

                                  -

                                  如果要实现对C++的支持,除了在上述基础上,我们还需增加以下几个内容:全局对象(cout)构造/析构的实现、new/delete、类的实现(string和iostream)。具体来说,会支持下面这个简单的代码:

                                  -
                                  #include "iostream"
                                  #include "string"
                                  using namespace std;

                                  int main(int argc, char* argv[])
                                  {
                                  string* msg = new string("Hello World");
                                  cout << *msg << endl;
                                  delete msg;
                                  return 0;
                                  }
                                  - -

                                  我们可以分步实现这些功能:

                                  -
                                    -
                                  1. new/delete实现

                                    -

                                    简单地使用运算符重载功能即可:

                                    -
                                    void* operator new(unsigned int size);
                                    void operator delete(void* p);
                                  2. -
                                  3. 类的实现

                                    -

                                    不多说

                                    -
                                  4. -
                                  5. 全局对象的构造/析构

                                    -
                                      -
                                    1. 构造

                                      -

                                      全局对象的构造在entry中进行:

                                      -
                                      void mini_crt_entry(void)
                                      {
                                      ...
                                      // 构造所有全局对象
                                      do_global_ctors();
                                      ret = main(argc,argv);
                                      }
                                      - -

                                      前文说过,在Linux中,每个.o文件的全局构造最后都会放在.ctor段。ld在链接阶段中将所有目标文件(包括用于标识.ctor段开始和结束的crtbegin.ocrtend.o)的.ctor段连在一起。所以,我们就需要实现三个文件:

                                      -
                                        -
                                      1. ctors.c

                                        -

                                        主要是用于实现do_global_ctors()。既然都有.ctor段存在了,那么它的实现就很简单,就是遍历.ctor段的所有函数指针并且调用它们。

                                        -
                                        void run_hooks();
                                        extern "C" void do_global_ctors()
                                        {
                                        run_hooks();
                                        }
                                        - -
                                        void run_hooks()
                                        {
                                        const ctor_func *list = ctors_begin;
                                        // 逐个调用ctors段里的东西
                                        while ((int)*++list != -1) (**list)();
                                        }
                                      2. -
                                      3. crtbegin.c

                                        -

                                        前文说到,按规定,ld将会以如下顺序连接.o文件:

                                        -
                                        ld crtbegin.o 其他文件 crtend.o -o test
                                        - -

                                        因而,crtbegin.c.ctor段会被链接在第一个。其作用是标识.ctor函数指针的数量,将在链接时由ld计算并且填写。因而在这里,我们只需将其初始化为一个特殊值(-1)就行:

                                        -
                                        typedef void (*ctor_func)(void);

                                        ctor_func ctors_begin[1] __attribute__((section(".ctors"))) = {
                                        (ctor_func)-1
                                        };
                                      4. -
                                      5. crtend.c

                                        -

                                        同样,crtend.c.ctor段标识着.ctor段的结束。因而我们也将其初始化为一个特殊值(-1):

                                        -
                                        typedef void (*ctor_func)(void);

                                        // 转化-1为函数指针,标识结束
                                        ctor_func crt_end[1] __attribute__((section(".ctors"))) = {
                                        (ctor_func) - 1
                                        };
                                      6. -
                                      -
                                    2. -
                                    3. 析构

                                      -

                                      全局对象的析构同样在entry中进行:

                                      -
                                      void mini_crt_entry(void)
                                      {
                                      ...
                                      ret = main(argc,argv);
                                      exit(ret);
                                      }

                                      void exit(int exitCode)
                                      {
                                      // 执行atexit,完成所有finit钩子
                                      mini_crt_call_exit_routine();
                                      // 调用exit系统调用
                                      asm( "movl %0,%%ebx \n\t"
                                      "movl $1,%%eax \n\t"
                                      "int $0x80 \n\t"
                                      "hlt \n\t"::"m"(exitCode));
                                      }
                                      - -

                                      具体也是以链表形式管理所有的函数指针,在atexit中注册(加入链表),在mini_crt_call_exit_routine中真正调用,不多分析。

                                      -
                                    4. -
                                    -
                                  6. -
                                  -
                                6. -
                                -

                                特辑:开发中遇到的链接小问题

                                  -
                                1. 已经在LD_LIBRARY_PATH中加入某个静态库的路径,但是仍然报错error while loading shared libraries: libssl.so.1.1: cannot open shared object file: No such file or directory

                                  -

                                  解决方法:执行sudo ldconfig

                                  -
                                  -

                                  ldconfig命令的作用就是将这些共享库的路径添加到动态链接器的缓存中,以便系统能够找到它们。

                                  -

                                  具体而言,ldconfig会检查默认的共享库路径(通常是/lib/usr/lib),以及在/etc/ld.so.conf/etc/ld.so.conf.d/目录中定义的其他路径。然后,它会更新动态链接器缓存,这样系统就知道在哪里查找共享库。

                                  -

                                  Q:也就是说ld不是像gcc找头文件,是根据-I选项规定的路径即时查找的,而是只根据缓存吗?所以尽管我们通过ld path环境变量设置了新查找路径,我们还是得手动刷新下ld缓存。

                                  -

                                  A:是的,你理解得很对。ldconfig主要是更新系统中的共享库缓存,而不是在每次程序运行时实时查找库文件的位置。动态链接器在运行时会根据缓存中的信息找到所需的共享库,以提高性能并避免在每次程序启动时重新搜索所有库。

                                  -

                                  当你通过LD_LIBRARY_PATH环境变量设置新的查找路径时,这只是告诉动态链接器在运行时应该额外搜索这些路径。然而,为了确保系统广泛地认识这些新的路径,以及其中的共享库,你需要运行ldconfig来更新缓存。

                                  -
                                  -
                                2. -
                                3. -
                                -]]> - - books - - - - 阅读JDK容器部分源码的心得体会1【Collection部分】 - /2022/10/16/%E9%98%85%E8%AF%BBJDK%E5%AE%B9%E5%99%A8%E9%83%A8%E5%88%86%E6%BA%90%E7%A0%81%E7%9A%84%E5%BF%83%E5%BE%97%E4%BD%93%E4%BC%9A1%E3%80%90Collection%E9%83%A8%E5%88%86%E3%80%91/ - -

                                idea 替换注释正则表达式/\*{1,2}[\s\S]*?\*/

                                -

                                typora 替换图片asset

                                -

                                \!\[.*\]\(D:\\aWorkStorage\\hexo\\blog\\source\\_posts\\阅读JDK容器部分源码的心得体会1【Collection部分】\\(.*)\.png\)

                                -

                                替换结果{% asset_img $1.png %}

                                -
                                - - - - -

                                迭代器相关接口

                                Iterable(I)

                                /*实现这个接口的类可用于for-each循环*/

                                public interface Iterable<T> {

                                Iterator<T> iterator();

                                /*对Iterator内每个元素实施此操作,直到遍历完或者抛出异常。*/
                                default void forEach(Consumer<? super T> action) {
                                Objects.requireNonNull(action);
                                for (T t : this) {
                                action.accept(t);
                                }
                                }

                                default Spliterator<T> spliterator() {
                                return Spliterators.spliteratorUnknownSize(iterator(), 0);
                                }
                                }
                                - -
                                -

                                引入迭代器的目的是为了**统一**“对容器里的元素进行遍历”这一操作。

                                -
                                -

                                Iterator(I)

                                public interface Iterator<E> {
                                //之后还有没有元素
                                boolean hasNext();

                                //返回当前所指元素,并且将iterator指针下移
                                E next();

                                //This call can be made only if neither remove nor add have been called after the last call to next or previous.
                                default void remove() {
                                throw new UnsupportedOperationException("remove");
                                }

                                default void forEachRemaining(Consumer<? super E> action) {
                                Objects.requireNonNull(action);
                                while (hasNext())
                                action.accept(next());
                                }
                                }
                                - -
                                -

                                注意,iterator并不指向具体元素,它指向的是元素的间隙

                                -

                                这些^就是iterator所指的位置。这样就能理解iterator的next和previous了吧2333

                                - - -

                                而set()所修改的元素,是其上一次调用next()或者previous()方法所返回的元素。

                                -

                                What’s the meaning of this source code of interface Collection in JAVA?

                                -
                                -

                                ListIterator(I)

                                public interface ListIterator<E> extends Iterator<E> {
                                boolean hasNext();
                                E next();
                                void remove();

                                //newly added as iterator below:
                                boolean hasPrevious();

                                E previous();

                                //返回后续调用 next 将返回的元素的索引。[应该就是当前元素索引]
                                int nextIndex();

                                int previousIndex();

                                //This call can be made only if neither remove nor add have been called after the last call to next or previous.
                                void set(E e);

                                //The element is inserted immediately before the element that would be returned by next, if any, and after the element that would be returned by previous, if any.
                                void add(E e);
                                }
                                - -
                                -

                                Note that the remove and set(Object) methods are not defined in terms of the cursor position; they are defined to operate on the last element returned by a call to next or previous().

                                -
                                -

                                Collection

                                Collection(I)

                                代码

                                /*
                                无直接实现类,一般用于需要使用多态传递参数的场合
                                其所有子类都必须有两个构造器: a void (no arguments) constructor, which creates an empty collection, and a constructor with a single argument of type Collection, which creates a new collection with the same elements as its argument. 这点语法上不会强制实现(因为接口不能强制构造方法),但其实是约定成俗的。

                                */
                                public interface Collection<E> extends Iterable<E> {

                                int size();

                                boolean isEmpty();

                                boolean contains(Object o);

                                Iterator<E> iterator();

                                //有关“safe”的问题讨论见下
                                Object[] toArray();

                                /*
                                Like the toArray() method, this method acts as bridge between array-based and
                                collection-based APIs. Further, this method allows precise control over the runtime
                                type of the output array, and may, under certain circumstances, be used to save
                                allocation costs.

                                Suppose x is a collection known to contain only strings. The following code can be
                                used to dump the collection into a newly allocated array of String:
                                String[] y = x.toArray(new String[0]);

                                说明还是有类型限制的
                                ArrayStoreException – if the runtime type of the specified array is not a supertype
                                of the runtime type of every element in this collection
                                */
                                <T> T[] toArray(T[] a);

                                /*
                                给集合增加一个元素。
                                If a collection refuses to add a particular element for any reason other than that it
                                already contains the element, it must throw an exception (rather than returning
                                false).
                                */
                                boolean add(E e);

                                boolean remove(Object o);

                                boolean containsAll(Collection<?> c);

                                //重复了怎么办
                                boolean addAll(Collection<? extends E> c);

                                //重复的会全弄走吗
                                boolean removeAll(Collection<?> c);

                                /*
                                删除此集合中满足给定谓词的所有元素。
                                在迭代期间或由谓词引发的错误或运行时异常将转发给调用者。
                                @return true if any elements were removed
                                */
                                default boolean removeIf(Predicate<? super E> filter) {
                                //非空filter
                                Objects.requireNonNull(filter);
                                boolean removed = false;
                                //迭代该集合
                                final Iterator<E> each = iterator();
                                while (each.hasNext()) {
                                if (filter.test(each.next())) {
                                each.remove();
                                removed = true;
                                }
                                }
                                return removed;
                                }

                                /*
                                把集合c中没有的元素全部移除
                                @return true if this collection changed as a result of the call
                                */
                                boolean retainAll(Collection<?> c);

                                void clear();

                                boolean equals(Object o);

                                /*
                                Any class that overrides the Object.equals method must also override the
                                Object.hashCode method.
                                c1.equals(c2) 相当于 c1.hashCode()==c2.hashCode().
                                */
                                int hashCode();

                                @Override
                                default Spliterator<E> spliterator() {
                                return Spliterators.spliterator(this, 0);
                                }

                                default Stream<E> stream() {
                                return StreamSupport.stream(spliterator(), false);
                                }

                                default Stream<E> parallelStream() {
                                return StreamSupport.stream(spliterator(), true);
                                }
                                }
                                - -

                                其中,

                                  -
                                1. “Bags or multisets (unordered collections that may contain duplicate elements) should implement this interface directly.”

                                  Set是不允许重复的元素集合的ADT,【ADT:抽象数据结构】

                                  -

                                  Bag是元素集合的ADT,允许重复.

                                  -

                                  通常,任何包含元素的东西都是Collection.

                                  -

                                  任何允许重复的集合都是Bag,否则就是Set.

                                  -

                                  通过索引访问元素的任何包都是List.

                                  -

                                  在最后一个之后附加新元素并且具有从头部(第一索引)移除元素的方法的Bag是Queue.

                                  -

                                  在最后一个之后附加新元素并且具有从尾部(最后一个索引)移除元素的方法的Bag是Stack.
                                  ————————————————
                                  原文链接:https://blog.csdn.net/weixin_34239718/article/details/114036886

                                  -
                                2. -
                                3. “destructive” methods 和”undestructive” methods

                                  这回答里写得很清楚:What are destructive and non-destructive methods in java?

                                  -
                                4. -
                                5. recursive traversal of the collection
                                  -

                                  Some collection operations which perform recursive traversal of the collection may fail with an exception for self-referential instances where the collection directly or indirectly contains itself.

                                  -
                                  -

                                  Java 8 vs Java 7 Collection Interface: self-referential instance

                                  -

                                  这个的第二个回答【较长的那个】写得很棒,较短的那个似乎是错误的。

                                  -

                                  正如描述所说的,“directly or indirectly contains itself”,回答里那个例子正是因为“indirectly contains itself”。

                                  -

                                  不仅仅是集合,每个Object都可能出现这样的错误(因为都包含有toString)

                                  -

                                  但是注意一点:

                                  -
                                  ArrayList l1 = new ArrayList();
                                  l1.add(l1);
                                  System.out.println(l1.toString());
                                  //输出:[(this Collection)]
                                  - -

                                  这段代码是正常的,是因为ArrayList里面toString的实现:

                                  -
                                  sb.append('[');
                                  for (;;) {
                                  E e = it.next();
                                  //注意此句
                                  sb.append(e == this ? "(this Collection)" : e);
                                  if (! it.hasNext())
                                  return sb.append(']').toString();
                                  sb.append(',').append(' ');
                                  }
                                  - -

                                  规避了这种风险。

                                  -

                                  下面的hashcode是不正常的,因为hashcode实现没有规避这种风险。

                                  -
                                6. -
                                7. 关于toArray的讨论
                                  For toArray() :
                                  The returned array will be "safe" in that no references to it are maintained by this collection. (In other words, this method must allocate a new array even if this collection is backed by an array). The caller is thus free to modify the returned array.
                                  - -
                                  List<String> list = Arrays.asList("foo", "bar", "baz");
                                  String[] array = list.toArray(new String[0]);
                                  array[0] = "qux";
                                  //修改数组不会修改列表
                                  System.out.println(list.get(0)); // still "foo"
                                  list.set(0,"haha");
                                  //修改列表也跟修改数组无关了
                                  System.out.println(list.get(0)+array[0]);
                                  - -
                                  ArrayList<Student> a = new ArrayList<>();
                                  a.add(new Student("Sarah",17));
                                  Student[] s = a.toArray(new Student[0]);
                                  //换一个引用对象
                                  s[0]=new Student("Lily",20);
                                  System.out.println(a.get(0)==s[0]);//false
                                  - -
                                  ArrayList<Student> a = new ArrayList<>();
                                  a.add(new Student("Sarah",17));
                                  Student[] s = a.toArray(new Student[0]);
                                  //修改引用对象
                                  s[0].name = "Lily";
                                  System.out.println(a.get(0).name);//Lily
                                  - -

                                  这也就说明,toArray()实际上是把list的元素复制一份弄成array,直接把值粘贴进去。

                                  -

                                  对于引用对象,list的元素实际上应该存的是对象在堆中的地址。所谓的“安全”指的是,修改array中的元素的值【也即对象地址】,也就是换一个气球牵,是不会影响原来list的元素的值的。

                                  -

                                  因而,对于样例1和2,我们其实给array的元素换了个气球牵,或者是把list换了个气球牵,相互对象不同,没什么影响。

                                  -

                                  对于样例3,我们修改了list和array共同指向的对象【就像C语言的指针那样】

                                  -

                                  以上参考自What does “Safe” mean in the Collections.toArray() JavaDoc?

                                  -
                                8. -
                                9. 关于default关键字

                                  java中default关键字

                                  -

                                  starkoverflow关于为什么要设立default的讨论:

                                  -

                                  What is the purpose of the default keyword in Java

                                  -

                                  Default methods were added to Java 8 primarily to support lambda expressions.

                                  -
                                10. -
                                -

                                AbstractCollection(A)

                                -

                                To implement an unmodifiable collection, the programmer needs only to extend this class and provide implementations for the iterator and size methods.
                                To implement a modifiable collection, the programmer must additionally[也要搞上面的] override this class’s add method (which otherwise throws an UnsupportedOperationException), and the iterator returned by the iterator method must additionally implement its remove method.

                                -
                                -
                                public abstract class AbstractCollection<E> implements Collection<E> {

                                //唯一的构造函数。 (用于子类构造函数的调用,通常是隐式的。)
                                protected AbstractCollection() {
                                }

                                // 查询操作
                                public abstract Iterator<E> iterator();
                                public abstract int size();
                                public boolean isEmpty() {
                                return size() == 0;
                                }

                                public boolean contains(Object o) {
                                Iterator<E> it = iterator();
                                if (o==null) {
                                while (it.hasNext())
                                if (it.next()==null)
                                return true;
                                } else {
                                while (it.hasNext())
                                if (o.equals(it.next()))
                                return true;
                                }
                                return false;
                                }


                                public Object[] toArray() {
                                // Estimate size of array; be prepared to see more or fewer elements
                                Object[] r = new Object[size()];
                                Iterator<E> it = iterator();
                                for (int i = 0; i < r.length; i++) {
                                if (! it.hasNext()) // fewer elements than expected
                                return Arrays.copyOf(r, i);
                                r[i] = it.next();
                                }
                                return it.hasNext() ? finishToArray(r, it) : r;
                                }


                                @SuppressWarnings("unchecked")
                                public <T> T[] toArray(T[] a) {
                                // Estimate size of array; be prepared to see more or fewer elements
                                int size = size();
                                //通过反射得到a同类型的数组实例
                                T[] r = a.length >= size ? a :
                                (T[])java.lang.reflect.Array
                                .newInstance(a.getClass().getComponentType(), size);
                                Iterator<E> it = iterator();

                                for (int i = 0; i < r.length; i++) {
                                if (! it.hasNext()) { // fewer elements than expected
                                if (a == r) {
                                r[i] = null; // null-terminate用null来标记数组结束。但如果数组里也有null该怎么办?从前面的contains来看也是有可能的
                                } else if (a.length < i) {
                                return Arrays.copyOf(r, i);
                                } else {
                                System.arraycopy(r, 0, a, 0, i);
                                if (a.length > i) {
                                a[i] = null;
                                }
                                }
                                return a;
                                }
                                r[i] = (T)it.next();
                                }
                                // more elements than expected
                                return it.hasNext() ? finishToArray(r, it) : r;
                                }

                                /*
                                为啥要-8?
                                Some VMs reserve some header words in an array. Attempts to allocate larger arrays may result in OutOfMemoryError: Requested array size exceeds VM limit
                                好像是因为要给8个字节保留作为数组的头标题。
                                */
                                private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;


                                //Reallocates the array being used within toArray when the iterator returned more elements than expected, and finishes filling it from the iterator.
                                @SuppressWarnings("unchecked")
                                private static <T> T[] finishToArray(T[] r, Iterator<?> it) {
                                int i = r.length;
                                while (it.hasNext()) {
                                int cap = r.length;
                                //说明此时需要扩容
                                if (i == cap) {
                                //每次扩当前大小的1/2
                                int newCap = cap + (cap >> 1) + 1;
                                // overflow-conscious code 溢出
                                if (newCap - MAX_ARRAY_SIZE > 0)
                                //cap+1 扩容后至少应该比原大小大1
                                newCap = hugeCapacity(cap + 1);
                                //申请一个船新数组空间
                                r = Arrays.copyOf(r, newCap);
                                }
                                r[i++] = (T)it.next();
                                }
                                // 如果过度扩容了就缩小回刚刚好
                                return (i == r.length) ? r : Arrays.copyOf(r, i);
                                }

                                private static int hugeCapacity(int minCapacity) {
                                //如果最小扩容也失败,说明要的东西太多了,救不了
                                if (minCapacity < 0) // overflow
                                throw new OutOfMemoryError
                                ("Required array size too large");
                                //否则
                                return (minCapacity > MAX_ARRAY_SIZE) ?
                                Integer.MAX_VALUE :
                                MAX_ARRAY_SIZE;
                                }

                                //修改操作

                                public boolean add(E e) {
                                throw new UnsupportedOperationException();
                                }

                                public boolean remove(Object o) {
                                Iterator<E> it = iterator();
                                if (o==null) {
                                while (it.hasNext()) {
                                if (it.next()==null) {
                                it.remove();
                                return true;
                                }
                                }
                                } else {
                                while (it.hasNext()) {
                                if (o.equals(it.next())) {
                                it.remove();
                                return true;
                                }
                                }
                                }
                                return false;
                                }

                                //批量操作
                                //O(n^2)
                                public boolean containsAll(Collection<?> c) {
                                for (Object e : c)
                                if (!contains(e))
                                return false;
                                return true;
                                }

                                public boolean addAll(Collection<? extends E> c) {
                                boolean modified = false;
                                for (E e : c)
                                if (add(e))
                                modified = true;
                                return modified;
                                }

                                public boolean removeAll(Collection<?> c) {
                                Objects.requireNonNull(c);
                                boolean modified = false;
                                Iterator<?> it = iterator();
                                while (it.hasNext()) {
                                if (c.contains(it.next())) {
                                it.remove();
                                modified = true;
                                }
                                }
                                return modified;
                                }

                                public boolean retainAll(Collection<?> c) {
                                Objects.requireNonNull(c);
                                boolean modified = false;
                                Iterator<E> it = iterator();
                                while (it.hasNext()) {
                                if (!c.contains(it.next())) {
                                it.remove();
                                modified = true;
                                }
                                }
                                return modified;
                                }

                                public void clear() {
                                Iterator<E> it = iterator();
                                while (it.hasNext()) {
                                it.next();
                                it.remove();
                                }
                                }

                                //字符串操作

                                public String toString() {
                                Iterator<E> it = iterator();
                                if (! it.hasNext())
                                return "[]";

                                StringBuilder sb = new StringBuilder();
                                sb.append('[');
                                for (;;) {
                                E e = it.next();
                                sb.append(e == this ? "(this Collection)" : e);
                                if (! it.hasNext())
                                return sb.append(']').toString();
                                sb.append(',').append(' ');
                                }
                                }
                                }
                                - -

                                其中:

                                -
                                  -
                                1. 在toArray方法中,为什么需要写这么奇怪的代码?

                                  what’s the usage of the code in the implementation of AbstractCollection’s toArray Method

                                  -
                                  Yes, you're right, as the javadoc sais, this method is prepared to return correctlly even if the Collection has been modified in the mean time.【并发安全】 That's why the initial size is just a hint. The usage of the iterator also ensures avoidance from the "concurrent modification" exception.
                                2. -
                                -

                                Queue

                                Queue(I)

                                - - -

                                The Queue interface does not define the blocking queue methods, which are common in concurrent programming. These methods, which wait for elements to appear or for space to become available, are defined in the java.util.concurrent.BlockingQueue interface, which extends this interface.

                                -

                                Queue implementations generally do not define element-based versions of methods equals and hashCode【就是不会像之前的list一样遍历一遍通过单个元素的hashcode计算整体的hashcode】 but instead inherit the identity based versions from class Object【hashcode由对象决定】, because element-based equality is not always well-defined for queues with the same elements but different ordering properties.

                                -

                                Each of these methods exists in two forms: one throws an exception if the operation fails, the other returns a special value (either null or false, depending on the operation). 容量受限的队列推荐使用第二种form

                                -
                                -

                                代码:

                                public interface Queue<E> extends Collection<E> {
                                /*
                                The offer method inserts an element if possible, otherwise returning false.
                                不同于Collection的add方法,offer添加失败时不会抛出异常,而是直接return false
                                */
                                boolean add(E e);
                                boolean offer(E e);
                                /*
                                The remove() and poll() methods differ only in their behavior
                                when the queue is empty:
                                the remove() method throws an exception, while the poll() method returns null.
                                */
                                E remove();
                                E poll();
                                /*
                                The element() and peek() methods return,
                                but do not remove, the head of the queue.
                                */
                                E element();
                                E peek();
                                }
                                - -

                                Deque(I)

                                -

                                双端队列。

                                -

                                The name deque is short for “double ended queue” and is usually pronounced “deck”.

                                - - -

                                This interface provides two methods to remove interior elements, removeFirstOccurrence and removeLastOccurrence.

                                - - - - - -
                                -

                                代码:

                                public interface Deque<E> extends Queue<E> {

                                void addFirst(E e);

                                void addLast(E e);

                                boolean offerFirst(E e);

                                boolean offerLast(E e);

                                E removeFirst();

                                E removeLast();

                                E pollFirst();

                                E pollLast();

                                E getFirst();

                                E getLast();

                                E peekFirst();

                                E peekLast();

                                boolean removeFirstOccurrence(Object o);

                                boolean removeLastOccurrence(Object o);

                                // *** Queue methods ***

                                boolean add(E e);

                                boolean offer(E e);

                                E remove();

                                E poll();

                                E element();

                                E peek();

                                // *** Stack methods ***

                                void push(E e);

                                E pop();

                                // *** Collection methods ***

                                boolean remove(Object o);

                                boolean contains(Object o);

                                public int size();

                                Iterator<E> iterator();
                                //Returns an iterator over the elements in this deque in reverse sequential order.
                                Iterator<E> descendingIterator();

                                }
                                - -

                                ArrayDeque

                                -

                                Resizable-array implementation of the Deque interface.

                                -

                                不允许空

                                -

                                Array deques have no capacity restrictions; they grow as necessary to support usage.

                                -

                                not thread-safe【相比于由vector实现的线程安全的Stack】

                                -

                                This class is likely to be faster than Stack when used as a stack, and faster than LinkedList when used as a queue.【6】

                                -

                                fail-fast

                                -
                                -

                                代码:

                                public class ArrayDeque<E> extends AbstractCollection<E>
                                implements Deque<E>, Cloneable, Serializable
                                {
                                //非private以让内部类能够访问到
                                /*
                                The capacity of the deque is the length of this array,
                                which is always a power of two. capacity只能是2的幂次
                                不能满【原理应该跟循环队列差不多,是怕头尾混淆】
                                但允许短暂的满之后马上扩容
                                所有不包含元素的数组单元为空
                                */
                                transient Object[] elements;

                                //The index of the element at the head of the deque
                                //(which is the element that would be removed by remove() or pop());
                                //or an arbitrary number equal to tail if the deque is empty.
                                //head在下标大的地方
                                transient int head;

                                //The index at which the 【next】 element would be added to the tail of the deque
                                //(via addLast(E), add(E), or push(E)).
                                //tail在下标小的地方
                                transient int tail;

                                private static final int MIN_INITIAL_CAPACITY = 8;

                                // ****** Array allocation and resizing utilities ******

                                private static int calculateSize(int numElements) {
                                int initialCapacity = MIN_INITIAL_CAPACITY;
                                // Find the best power of two to hold elements.
                                // Tests "<=" because arrays aren't kept full.
                                if (numElements >= initialCapacity) {
                                //原理类似HashMap.tableSizeFor
                                //这一通操作可以得到比cap大的,且离cap最近的2的幂次方数
                                initialCapacity = numElements;
                                initialCapacity |= (initialCapacity >>> 1);
                                initialCapacity |= (initialCapacity >>> 2);
                                initialCapacity |= (initialCapacity >>> 4);
                                initialCapacity |= (initialCapacity >>> 8);
                                initialCapacity |= (initialCapacity >>> 16);
                                initialCapacity++;

                                if (initialCapacity < 0) // Too many elements, must back off
                                initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
                                }
                                return initialCapacity;
                                }


                                private void allocateElements(int numElements) {
                                elements = new Object[calculateSize(numElements)];
                                }


                                private void doubleCapacity() {
                                assert head == tail;
                                int p = head;
                                int n = elements.length;
                                int r = n - p; // number of elements to the right of p
                                int newCapacity = n << 1;
                                if (newCapacity < 0)
                                throw new IllegalStateException("Sorry, deque too big");
                                Object[] a = new Object[newCapacity];
                                //以head==tail为分界线,右边那段移到开头,左边那段移到后面
                                System.arraycopy(elements, p, a, 0, r);
                                System.arraycopy(elements, 0, a, r, p);
                                elements = a;
                                head = 0;
                                tail = n;
                                }

                                private <T> T[] copyElements(T[] a) {
                                if (head < tail) {
                                System.arraycopy(elements, head, a, 0, size());
                                } else if (head > tail) {
                                int headPortionLen = elements.length - head;
                                System.arraycopy(elements, head, a, 0, headPortionLen);
                                System.arraycopy(elements, 0, a, headPortionLen, tail);
                                }
                                return a;
                                }

                                //1
                                public ArrayDeque() {
                                elements = new Object[16];
                                }

                                public ArrayDeque(int numElements) {
                                allocateElements(numElements);
                                }

                                public ArrayDeque(Collection<? extends E> c) {
                                allocateElements(c.size());
                                addAll(c);
                                }

                                // The main insertion and extraction methods are addFirst,
                                // addLast, pollFirst, pollLast. The other methods are defined in
                                // terms of these.就是说这几个最重要,别的方法都是这四个的附庸

                                public void addFirst(E e) {
                                if (e == null)
                                throw new NullPointerException();
                                //2
                                elements[head = (head - 1) & (elements.length - 1)] = e;
                                if (head == tail)
                                //队列满
                                doubleCapacity();
                                }

                                public void addLast(E e) {
                                if (e == null)
                                throw new NullPointerException();
                                elements[tail] = e;
                                if ( (tail = (tail + 1) & (elements.length - 1)) == head)
                                //队列满
                                doubleCapacity();
                                }

                                public boolean offerFirst(E e) {
                                addFirst(e);
                                return true;
                                }

                                public boolean offerLast(E e) {
                                addLast(e);
                                return true;
                                }

                                public E removeFirst() {
                                E x = pollFirst();
                                if (x == null)
                                throw new NoSuchElementException();
                                return x;
                                }

                                public E removeLast() {
                                E x = pollLast();
                                if (x == null)
                                throw new NoSuchElementException();
                                return x;
                                }

                                public E pollFirst() {
                                int h = head;
                                @SuppressWarnings("unchecked")
                                E result = (E) elements[h];
                                // Element is null if deque empty
                                if (result == null)
                                return null;
                                elements[h] = null; // Must null out slot
                                head = (h + 1) & (elements.length - 1);
                                return result;
                                }

                                public E pollLast() {
                                int t = (tail - 1) & (elements.length - 1);
                                @SuppressWarnings("unchecked")
                                E result = (E) elements[t];
                                if (result == null)
                                return null;
                                elements[t] = null;
                                tail = t;
                                return result;
                                }

                                public E getFirst() {
                                @SuppressWarnings("unchecked")
                                E result = (E) elements[head];
                                if (result == null)
                                throw new NoSuchElementException();
                                return result;
                                }

                                public E getLast() {
                                @SuppressWarnings("unchecked")
                                E result = (E) elements[(tail - 1) & (elements.length - 1)];
                                if (result == null)
                                throw new NoSuchElementException();
                                return result;
                                }

                                @SuppressWarnings("unchecked")
                                public E peekFirst() {
                                // elements[head] is null if deque empty
                                return (E) elements[head];
                                }

                                @SuppressWarnings("unchecked")
                                public E peekLast() {
                                return (E) elements[(tail - 1) & (elements.length - 1)];
                                }

                                public boolean removeFirstOccurrence(Object o) {
                                if (o == null)
                                return false;
                                //掩码
                                int mask = elements.length - 1;
                                int i = head;
                                Object x;
                                while ( (x = elements[i]) != null) {
                                if (o.equals(x)) {
                                delete(i);
                                return true;
                                }
                                //头->尾
                                i = (i + 1) & mask;
                                }
                                return false;
                                }

                                public boolean removeLastOccurrence(Object o) {
                                if (o == null)
                                return false;
                                int mask = elements.length - 1;
                                int i = (tail - 1) & mask;
                                Object x;
                                while ( (x = elements[i]) != null) {
                                if (o.equals(x)) {
                                delete(i);
                                return true;
                                }
                                //尾->头
                                i = (i - 1) & mask;
                                }
                                return false;
                                }

                                // *** Queue methods ***

                                public boolean add(E e) {
                                addLast(e);
                                return true;
                                }

                                public boolean offer(E e) {
                                return offerLast(e);
                                }

                                public E remove() {
                                return removeFirst();
                                }

                                public E poll() {
                                return pollFirst();
                                }

                                public E element() {
                                return getFirst();
                                }

                                public E peek() {
                                return peekFirst();
                                }

                                // *** Stack methods ***

                                public void push(E e) {
                                addFirst(e);
                                }

                                public E pop() {
                                return removeFirst();
                                }

                                //检查队列情况正常
                                private void checkInvariants() {
                                assert elements[tail] == null;
                                //如果成立,只能是队列空;不成立的话,不能有空元素
                                assert head == tail ? elements[head] == null :
                                (elements[head] != null &&
                                elements[(tail - 1) & (elements.length - 1)] != null);
                                assert elements[(head - 1) & (elements.length - 1)] == null;
                                }

                                //Returns: true if elements moved backwards而不是操作是否成功
                                private boolean delete(int i) {
                                checkInvariants();
                                final Object[] elements = this.elements;
                                final int mask = elements.length - 1;
                                final int h = head;
                                final int t = tail;
                                final int front = (i - h) & mask;
                                final int back = (t - i) & mask;

                                // Invariant: head <= i < tail mod circularity
                                if (front >= ((t - h) & mask))
                                throw new ConcurrentModificationException();

                                // Optimize for least element motion
                                if (front < back) {
                                if (h <= i) {
                                //把i覆盖掉了
                                System.arraycopy(elements, h, elements, h + 1, front);
                                } else { // Wrap around
                                System.arraycopy(elements, 0, elements, 1, i);
                                elements[0] = elements[mask];
                                System.arraycopy(elements, h, elements, h + 1, mask - h);
                                }
                                elements[h] = null;
                                head = (h + 1) & mask;
                                return false;
                                } else {
                                if (i < t) { // Copy the null tail as well
                                System.arraycopy(elements, i + 1, elements, i, back);
                                //注意没设置空,因为确实不用
                                tail = t - 1;
                                } else { // Wrap around
                                System.arraycopy(elements, i + 1, elements, i, mask - i);
                                elements[mask] = elements[0];
                                System.arraycopy(elements, 1, elements, 0, t);
                                tail = (t - 1) & mask;
                                }
                                return true;
                                }
                                }

                                // *** Collection Methods ***

                                public int size() {
                                return (tail - head) & (elements.length - 1);
                                }

                                public boolean isEmpty() {
                                return head == tail;
                                }

                                //The elements will be ordered from first (head) to last (tail).
                                //This is the same order that elements would be dequeued
                                //(via successive calls to remove or popped (via successive calls to pop).
                                public Iterator<E> iterator() {
                                return new DeqIterator();
                                }

                                public Iterator<E> descendingIterator() {
                                return new DescendingIterator();
                                }

                                private class DeqIterator implements Iterator<E> {

                                private int cursor = head;

                                //Tail recorded at construction (also in remove), to stop iterator[怪不得叫fence]
                                //and also to check for comodification[检查并发].
                                private int fence = tail;

                                private int lastRet = -1;

                                public boolean hasNext() {
                                return cursor != fence;
                                }

                                public E next() {
                                if (cursor == fence)
                                throw new NoSuchElementException();
                                @SuppressWarnings("unchecked")
                                E result = (E) elements[cursor];
                                // This check doesn't catch all possible comodifications,
                                // but does catch the ones that corrupt traversal【破坏遍历的】
                                //tail!=fence说明迭代时修改。
                                if (tail != fence || result == null)
                                throw new ConcurrentModificationException();
                                lastRet = cursor;
                                cursor = (cursor + 1) & (elements.length - 1);
                                return result;
                                }

                                public void remove() {
                                if (lastRet < 0)
                                throw new IllegalStateException();
                                //3
                                if (delete(lastRet)) { // if left-shifted, undo increment in next()
                                cursor = (cursor - 1) & (elements.length - 1);
                                //update
                                fence = tail;
                                }
                                lastRet = -1;
                                }

                                public void forEachRemaining(Consumer<? super E> action) {
                                Objects.requireNonNull(action);
                                Object[] a = elements;
                                int m = a.length - 1, f = fence, i = cursor;
                                //4执行完后直接迭代结束
                                cursor = f;
                                while (i != f) {
                                @SuppressWarnings("unchecked") E e = (E)a[i];
                                i = (i + 1) & m;
                                if (e == null)
                                throw new ConcurrentModificationException();
                                action.accept(e);
                                }
                                }
                                }

                                private class DescendingIterator implements Iterator<E> {

                                private int cursor = tail;
                                //终点为fence,此时终点为head
                                private int fence = head;
                                private int lastRet = -1;

                                public boolean hasNext() {
                                return cursor != fence;
                                }

                                public E next() {
                                if (cursor == fence)
                                throw new NoSuchElementException();
                                cursor = (cursor - 1) & (elements.length - 1);
                                @SuppressWarnings("unchecked")
                                E result = (E) elements[cursor];
                                if (head != fence || result == null)
                                throw new ConcurrentModificationException();
                                lastRet = cursor;
                                return result;
                                }

                                public void remove() {
                                if (lastRet < 0)
                                throw new IllegalStateException();
                                if (!delete(lastRet)) {
                                cursor = (cursor + 1) & (elements.length - 1);
                                fence = head;
                                }
                                lastRet = -1;
                                }
                                //不支持for-each了吧233
                                }

                                public boolean contains(Object o) {
                                if (o == null)
                                return false;
                                int mask = elements.length - 1;
                                int i = head;
                                Object x;
                                while ( (x = elements[i]) != null) {
                                if (o.equals(x))
                                return true;
                                i = (i + 1) & mask;
                                }
                                return false;
                                }

                                public boolean remove(Object o) {
                                return removeFirstOccurrence(o);
                                }

                                public void clear() {
                                int h = head;
                                int t = tail;
                                if (h != t) { // clear all cells
                                head = tail = 0;
                                int i = h;
                                int mask = elements.length - 1;
                                do {
                                elements[i] = null;
                                i = (i + 1) & mask;
                                } while (i != t);
                                }
                                }

                                public Object[] toArray() {
                                return copyElements(new Object[size()]);
                                }

                                @SuppressWarnings("unchecked")
                                public <T> T[] toArray(T[] a) {
                                int size = size();
                                if (a.length < size)
                                a = (T[])java.lang.reflect.Array.newInstance(
                                a.getClass().getComponentType(), size);
                                copyElements(a);
                                if (a.length > size)
                                a[size] = null;
                                return a;
                                }

                                // *** Object methods ***

                                public ArrayDeque<E> clone() {
                                try {
                                @SuppressWarnings("unchecked")
                                ArrayDeque<E> result = (ArrayDeque<E>) super.clone();
                                result.elements = Arrays.copyOf(elements, elements.length);
                                return result;
                                } catch (CloneNotSupportedException e) {
                                throw new AssertionError();
                                }
                                }

                                private static final long serialVersionUID = 2340985798034038923L;

                                private void writeObject(java.io.ObjectOutputStream s)
                                throws java.io.IOException {...}

                                private void readObject(java.io.ObjectInputStream s)
                                throws java.io.IOException, ClassNotFoundException {...}

                                public Spliterator<E> spliterator() {
                                return new DeqSpliterator<E>(this, -1, -1);
                                }

                                static final class DeqSpliterator<E> implements Spliterator<E> {...}

                                }
                                - -

                                其中:

                                  -
                                1. 默认容量

                                  空构造器的默认容量为16

                                  -
                                2. -
                                3. (head - 1) & (elements.length - 1)

                                  是一个便捷的截位取余操作,这跟hashmap一个原理,详见hashmap第二点。

                                  -
                                4. -
                                5. if (delete(lastRet))

                                  delete方法返回true,说明右移数组,此时next指针需要++

                                  -

                                  delete方法返回false,说明左移数组,此时next指针不变

                                  -
                                6. -
                                7. forEachRemaining

                                  跟差不多所有的迭代器实现一样,此方法执行完毕之后,cursor直接跳到数组最末,相当于迭代结束

                                  -
                                8. -
                                -

                                List

                                List(I)

                                有序、支持随机访问

                                -

                                代码

                                /*
                                List 接口提供了一个特殊的迭代器,称为 ListIterator,
                                除了 Iterator 接口提供的正常操作之外,它还允许元素插入和替换以及双向访问。
                                List 接口提供了两种方法来搜索指定的对象。 从性能的角度来看,应谨慎使用这些方法。
                                在许多实现中,它们将执行代价高昂的线性搜索。
                                Note: While it is permissible for lists to contain themselves as elements, extreme caution is advised: the equals and hashCode methods are no longer well defined on such a list.【这跟上面的引用点4是一样的。】

                                */
                                public interface List<E> extends Collection<E> {
                                int size();
                                boolean isEmpty();
                                boolean contains(Object o);
                                Iterator<E> iterator();
                                Object[] toArray();
                                <T> T[] toArray(T[] a);
                                boolean add(E e);
                                boolean remove(Object o);
                                boolean containsAll(Collection<?> c);
                                boolean addAll(Collection<? extends E> c);
                                boolean removeAll(Collection<?> c);
                                boolean retainAll(Collection<?> c);
                                //Returns true if and only if the specified object is also a list,
                                //both lists have the same size, and all corresponding pairs of elements in the two lists are equal.
                                //In other words, two lists are defined to be equal if they contain the same elements in the same order.
                                //注意,对于未重载equal方法的类,引用对象的相等指的是地址相等也就是说必须是一模一样的对象,两个不同对象但值相同,这种情况是不算equal的。
                                boolean equals(Object o);
                                int hashCode();

                                //newly add or change below

                                //将此列表的每个元素替换为将运算符应用于该元素的结果。
                                default void replaceAll(UnaryOperator<E> operator) {
                                Objects.requireNonNull(operator);
                                final ListIterator<E> li = this.listIterator();
                                while (li.hasNext()) {
                                li.set(operator.apply(li.next()));
                                }
                                }

                                //@SuppressWarings注解 作用:用于抑制编译器产生警告信息。
                                @SuppressWarnings({"unchecked", "rawtypes"})
                                default void sort(Comparator<? super E> c) {
                                Object[] a = this.toArray();
                                //借助Arrays的sort方法
                                Arrays.sort(a, (Comparator) c);
                                ListIterator<E> i = this.listIterator();
                                //再线性逐一替换
                                for (Object e : a) {
                                i.next();
                                // set:
                                // Replaces the last element returned by next or previous
                                // with the specified element
                                i.set((E) e);
                                }
                                }

                                E get(int index);

                                E set(int index, E element);

                                //插入元素,把当前位及其以后的元素都往后挪一位
                                void add(int index, E element);

                                //移除元素,把当前位及其以后的元素都往前挪一位
                                E remove(int index);

                                //-1 if this list does not contain the element
                                //ClassCastException:if the type of the specified element
                                //is incompatible with this list
                                int indexOf(Object o);

                                ListIterator<E> listIterator();

                                //指定的索引指示初始调用next将返回的第一个元素.对 previous 的初始调用将返回具有指定索引减一的元素。
                                ListIterator<E> listIterator(int index);

                                //[fromIndex,toIndex).If fromIndex==toIndex then return==null.
                                List<E> subList(int fromIndex, int toIndex);

                                @Override
                                default Spliterator<E> spliterator() {
                                return Spliterators.spliterator(this, Spliterator.ORDERED);
                                }

                                }
                                - -

                                其中:

                                  -
                                1. hashcode不允许自环

                                  Note: While it is permissible for lists to contain themselves as elements, extreme caution is advised: the equals and hashCode methods are no longer well defined on such a list.【这跟上面的引用点4是差不多的。】

                                  -
                                  ArrayList l1 = new ArrayList();
                                  l1.add(l1);
                                  System.out.println(l1.hashCode());
                                  //输出:Stack Overflow
                                2. -
                                3. structural and non-structural change in list

                                  Difference between structural and non-structural for lists

                                  -
                                4. -
                                5. 关于sublist
                                  ① view

                                  sublist的document有一个很有趣的点,就是把sublist称为原list的视图view,这不禁让人想起了数据库里的表和表的视图。

                                  -

                                  但是还是有差别的。

                                  -

                                  在数据库中,视图仅仅是表或表的一部分的快照,修改视图对原表没有影响。但此处,对sublist的结构性修改和非结构性修改都会使原list的对应元素发生改变。

                                  -

                                  但有一点是相同的。如果对原表/原list修改,那么视图就会没用/会寄掉。

                                  -

                                  可以像这样来对主list指定范围内的元素进行操作,免去复杂的下标。

                                  -
                                  //For example, the following idiom removes a range of elements from a list:
                                  list.subList(from, to).clear();
                                  +

                                  之后,我们将其以如下参数编译为静态库:

                                  +
                                  $ gcc -c -fno-builtin -nostdlib -fno-stack-protector entry.c malloc.c stdio.c string.c printf.c
                                  $ ar -rs minicrt.a malloc.o printf.o stdio.o string.o
                                  # 编译测试用例
                                  $ gcc -m32 -c -ggdb -fno-builtin -nostdlib -fno-stack-protector test.c
                                  -
                                  ② 留下一个问题
                                  public class Main {
                                  public static void main(String[] args) {
                                  ArrayList<Student> a=new ArrayList<>();
                                  a.add(new Student("Lily",16));
                                  a.add(new Student("Sam",16));
                                  a.add(new Student("Tom",16));
                                  a.add(new Student("Mary",16));
                                  a.add(new Student("Mark",16));
                                  a.add(new Student("John",16));

                                  List<Student> suba=a.subList(2,5);

                                  System.out.println("Firstly,suba:");
                                  printall(suba);

                                  System.out.println("Change the value of age:");
                                  suba.get(1).age=300;
                                  System.out.println("suba:");
                                  printall(suba);
                                  System.out.println("a:");
                                  printall(a);

                                  System.out.println("Change the address:");
                                  suba.set(1,new Student("haha",200));
                                  System.out.println("suba:");
                                  printall(suba);
                                  System.out.println("a:");
                                  printall(a);

                                  System.out.println("Change the structure of sublist:");
                                  suba.remove(0);
                                  System.out.println("suba:");
                                  printall(suba);
                                  System.out.println("a:");
                                  printall(a);
                                  }
                                  public static void printall(List<Student> list){
                                  for (Student a : list){
                                  System.out.print(a+"\t");
                                  }
                                  System.out.println();
                                  }
                                  }
                                  /*运行结果
                                  Firstly,suba:
                                  name :Tomage :16 name :Maryage :16 name :Markage :16
                                  Change the value of age:
                                  suba:
                                  name :Tomage :16 name :Maryage :300 name :Markage :16
                                  a:
                                  name :Lilyage :16 name :Samage :16 name :Tomage :16 name :Maryage :300 name :Markage :16 name :Johnage :16
                                  Change the address:
                                  suba:
                                  name :Tomage :16 name :hahaage :200 name :Markage :16
                                  a:
                                  name :Lilyage :16 name :Samage :16 name :Tomage :16 name :hahaage :200 name :Markage :16 name :Johnage :16
                                  Change the structure of sublist:
                                  suba:
                                  name :hahaage :200 name :Markage :16
                                  a:
                                  name :Lilyage :16 name :Samage :16 name :hahaage :200 name :Markage :16 name :Johnage :16
                                  */
                                  +

                                  再指定mini_crt_entry为入口进行静态链接:

                                  +
                                  $ ld -m elf_i386 -static -e mini_crt_entry entry.o test.o minicrt.a -o test
                                6. +
                                7. C++

                                  +

                                  如果要实现对C++的支持,除了在上述基础上,我们还需增加以下几个内容:全局对象(cout)构造/析构的实现、new/delete、类的实现(string和iostream)。具体来说,会支持下面这个简单的代码:

                                  +
                                  #include "iostream"
                                  #include "string"
                                  using namespace std;

                                  int main(int argc, char* argv[])
                                  {
                                  string* msg = new string("Hello World");
                                  cout << *msg << endl;
                                  delete msg;
                                  return 0;
                                  }
                                  -

                                  为什么之前的toArray得到的array换了个对象,原list不会变【详见Collection说明第五点】;而这里的sublist得到的子list换了个对象,原list也会变呢?

                                  -

                                  也许这跟间接寻址的级数有关?之后看过ArrayList的具体实现再来解答。

                                  -
                                  -

                                  穿越回来:

                                  -

                                  ​ toArray得到array,是新开辟了存储空间,里面存放了原list对象的地址。因而,修改新存储空间的地址内容,原list是不变的。

                                  -

                                  ​ 但sublist连新开辟存储空间也没有,其差不多所有操作都是从list进行的。因而它换了个对象,具体实现就是让原list也换了个对象。

                                  -

                                  ​ 所以说其实前者更像是数据库里面的“视图”概念。

                                  -
                                  -
                                  ③ 关于sublis和原list、non-structral和structural的区别:

                                  Can structural changes in sublist reflected in the original list, in JAVA?

                                  -
                                  -

                                  In the document of List class:

                                  -
                                  /*
                                  The returned list is backed by this list, so non-structural changes in the returned list are reflected in this list, and vice-versa.
                                  */
                                  List<E> subList(int fromIndex, int toIndex);
                                  +

                                  我们可以分步实现这些功能:

                                  +
                                    +
                                  1. new/delete实现

                                    +

                                    简单地使用运算符重载功能即可:

                                    +
                                    void* operator new(unsigned int size);
                                    void operator delete(void* p);
                                  2. +
                                  3. 类的实现

                                    +

                                    不多说

                                    +
                                  4. +
                                  5. 全局对象的构造/析构

                                    +
                                      +
                                    1. 构造

                                      +

                                      全局对象的构造在entry中进行:

                                      +
                                      void mini_crt_entry(void)
                                      {
                                      ...
                                      // 构造所有全局对象
                                      do_global_ctors();
                                      ret = main(argc,argv);
                                      }
                                      -

                                      Can structural changes in sublist reflected in the original list in JAVA?

                                      -
                                  -

                                  答案是会改变的。实例可看问题中代码,或者point3的代码。

                                  -

                                  但是既然都会变的话,为什么文档里面要特意强调“non-structural”呢?这是因为,对sublist进行结构性改变会让原list也正常地一起变,但是对原list进行结构性改变却会让sublist寄掉:

                                  -

                                  文档下面接着写道:

                                  -
                                  The semantics of the list returned by this method become undefined if the backing list (i.e., this list) is structurally modified in any way other than via the returned list. (Structural modifications are those that change the size of this list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results.)
                                  /*
                                  sublist会寄,如果其原list被结构性改变了,且是以除了通过sublist结构性改变的方式外的其他方式改变的。(结构性改变是指那些会改变list的长度,或者会使迭代失败的那些改变)
                                  */
                                  +

                                  前文说过,在Linux中,每个.o文件的全局构造最后都会放在.ctor段。ld在链接阶段中将所有目标文件(包括用于标识.ctor段开始和结束的crtbegin.ocrtend.o)的.ctor段连在一起。所以,我们就需要实现三个文件:

                                  +
                                    +
                                  1. ctors.c

                                    +

                                    主要是用于实现do_global_ctors()。既然都有.ctor段存在了,那么它的实现就很简单,就是遍历.ctor段的所有函数指针并且调用它们。

                                    +
                                    void run_hooks();
                                    extern "C" void do_global_ctors()
                                    {
                                    run_hooks();
                                    }
                                    -

                                    通过代码验证可得,确实寄了

                                    -
                                    public static void main(String[] args) {
                                    ArrayList<Student> a=new ArrayList<>();
                                    a.add(new Student("Lily",16));
                                    a.add(new Student("Sam",16));
                                    a.add(new Student("Tom",16));
                                    a.add(new Student("Mary",16));
                                    a.add(new Student("Mark",16));
                                    a.add(new Student("John",16));

                                    List<Student> suba=a.subList(2,5);

                                    System.out.println("Firstly,suba:");
                                    printall(suba);
                                    printall(a);

                                    System.out.println("Change the origin structure:");
                                    a.remove(3);
                                    System.out.println("a:");
                                    printall(a);
                                    System.out.println("suba:");
                                    printall(suba);
                                    }
                                    /*运行结果
                                    Firstly,suba:
                                    name :Tomage :16 name :Maryage :16 name :Markage :16
                                    name :Lilyage :16 name :Samage :16 name :Tomage :16 name :Maryage :16 name :Markage :16 name :Johnage :16
                                    Change the origin structure:
                                    a:
                                    name :Lilyage :16 name :Samage :16 name :Tomage :16 name :Markage :16 name :Johnage :16
                                    suba:
                                    Exception in thread "main" java.util.ConcurrentModificationException
                                    at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1241)
                                    */
                                    +
                                    void run_hooks()
                                    {
                                    const ctor_func *list = ctors_begin;
                                    // 逐个调用ctors段里的东西
                                    while ((int)*++list != -1) (**list)();
                                    }
                                  2. +
                                  3. crtbegin.c

                                    +

                                    前文说到,按规定,ld将会以如下顺序连接.o文件:

                                    +
                                    ld crtbegin.o 其他文件 crtend.o -o test
                                    -

                                    通过以上可以感觉,估计sublist和原list的元素是共享存储空间的,只不过可能对象里有相关维护的变量。Any method accessing the list through the sub list effectively does index + offset.故而要是list变了,sublist的相关维护变量不变,就会依然傻傻地进行offset+index操作,这样就会寄。此猜想有待验证23333

                                    -
                                    -

                                    穿越回来:

                                    -
                                      -
                                    1. 确实是共享存储空间的,不如说sublist就直接引用了原list的变量,所有操作实质上都是在原list上进行。
                                    2. -
                                    3. sublist傻傻地进行offset+index操作,这样体现在原list上,可能导致下标越界或者结果并非我们想要的。
                                    4. +

                                      因而,crtbegin.c.ctor段会被链接在第一个。其作用是标识.ctor函数指针的数量,将在链接时由ld计算并且填写。因而在这里,我们只需将其初始化为一个特殊值(-1)就行:

                                      +
                                      typedef void (*ctor_func)(void);

                                      ctor_func ctors_begin[1] __attribute__((section(".ctors"))) = {
                                      (ctor_func)-1
                                      };
                                      +
                                    5. crtend.c

                                      +

                                      同样,crtend.c.ctor段标识着.ctor段的结束。因而我们也将其初始化为一个特殊值(-1):

                                      +
                                      typedef void (*ctor_func)(void);

                                      // 转化-1为函数指针,标识结束
                                      ctor_func crt_end[1] __attribute__((section(".ctors"))) = {
                                      (ctor_func) - 1
                                      };
                                    -
                                  4. -
                                  -

                                  AbstractList(A)

                                  -

                                  提供随机访问list的基本骨架

                                  -

                                  To implement an unmodifiable list, the programmer needs only to extend this class and provide implementations for the get(int) and size() methods.

                                  -

                                  To implement a modifiable list, the programmer must additionally override the set(int, Object) method

                                  -

                                  If the list is variable-size,the programmer must additionally override the add(int, E) and remove(int) methods.

                                  -

                                  Unlike the other abstract collection implementations, the programmer does not have to provide an iterator implementation; the iterator and list iterator are implemented by this class, on top of the “random access“ methods: get(int), set(int, E), add(int, E) and remove(int).这里的迭代器反而是通过类里面的方法来实现的

                                  -
                                  -

                                  代码

                                  public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {

                                  protected AbstractList() {
                                  }
                                  //重写add方法
                                  public boolean add(E e) {
                                  //此为下面的public void add(int index, E element);
                                  add(size(), e);
                                  return true;
                                  }
                                  //交给具体实现
                                  abstract public E get(int index);
                                  //假设不能修改
                                  public E set(int index, E element) {
                                  throw new UnsupportedOperationException();
                                  }
                                  public void add(int index, E element) {
                                  throw new UnsupportedOperationException();
                                  }
                                  public E remove(int index) {
                                  throw new UnsupportedOperationException();
                                  }

                                  // Search Operations

                                  public int indexOf(Object o) {
                                  ListIterator<E> it = listIterator();
                                  if (o==null) {
                                  while (it.hasNext())
                                  if (it.next()==null)
                                  return it.previousIndex();
                                  } else {
                                  while (it.hasNext())
                                  if (o.equals(it.next()))
                                  return it.previousIndex();
                                  }
                                  return -1;
                                  }

                                  public int lastIndexOf(Object o) {
                                  //从后往前遍历
                                  ListIterator<E> it = listIterator(size());
                                  if (o==null) {
                                  while (it.hasPrevious())
                                  if (it.previous()==null)
                                  return it.nextIndex();
                                  } else {
                                  while (it.hasPrevious())
                                  if (o.equals(it.previous()))
                                  return it.nextIndex();
                                  }
                                  return -1;
                                  }

                                  // Bulk Operations

                                  public void clear() {
                                  removeRange(0, size());
                                  }

                                  public boolean addAll(int index, Collection<? extends E> c) {
                                  rangeCheckForAdd(index);
                                  boolean modified = false;
                                  for (E e : c) {
                                  add(index++, e);
                                  modified = true;
                                  }
                                  return modified;
                                  }

                                  // Iterators

                                  public Iterator<E> iterator() {
                                  return new Itr();
                                  }


                                  public ListIterator<E> listIterator() {
                                  return listIterator(0);
                                  }


                                  public ListIterator<E> listIterator(final int index) {
                                  rangeCheckForAdd(index);

                                  return new ListItr(index);
                                  }

                                  //内部类诶 1
                                  private class Itr implements Iterator<E> {
                                  //Index of element to be returned by subsequent call to next.
                                  int cursor = 0;
                                  //Index of element returned by most recent call to next or previous. Reset to -1 if this element is deleted by a call to remove.
                                  int lastRet = -1;

                                  int expectedModCount = modCount;

                                  public boolean hasNext() {
                                  return cursor != size();
                                  }

                                  public E next() {
                                  checkForComodification();
                                  try {
                                  int i = cursor;
                                  E next = get(i);
                                  lastRet = i;
                                  cursor = i + 1;
                                  return next;
                                  } catch (IndexOutOfBoundsException e) {
                                  checkForComodification();
                                  throw new NoSuchElementException();
                                  }
                                  }

                                  public void remove() {
                                  if (lastRet < 0)
                                  throw new IllegalStateException();
                                  checkForComodification();

                                  try {
                                  AbstractList.this.remove(lastRet);
                                  if (lastRet < cursor)
                                  cursor--;
                                  lastRet = -1;
                                  expectedModCount = modCount;
                                  } catch (IndexOutOfBoundsException e) {
                                  throw new ConcurrentModificationException();
                                  }
                                  }

                                  final void checkForComodification() {
                                  if (modCount != expectedModCount)
                                  throw new ConcurrentModificationException();
                                  }
                                  }

                                  private class ListItr extends Itr implements ListIterator<E> {
                                  ListItr(int index) {
                                  cursor = index;
                                  }

                                  public boolean hasPrevious() {
                                  return cursor != 0;
                                  }

                                  public E previous() {
                                  checkForComodification();
                                  try {
                                  int i = cursor - 1;
                                  E previous = get(i);
                                  //lastRet=i;cursor=i;
                                  lastRet = cursor = i;
                                  return previous;
                                  } catch (IndexOutOfBoundsException e) {
                                  checkForComodification();
                                  throw new NoSuchElementException();
                                  }
                                  }

                                  public int nextIndex() {
                                  return cursor;
                                  }

                                  public int previousIndex() {
                                  return cursor-1;
                                  }

                                  public void set(E e) {
                                  if (lastRet < 0)
                                  throw new IllegalStateException();
                                  checkForComodification();

                                  try {
                                  AbstractList.this.set(lastRet, e);
                                  expectedModCount = modCount;
                                  } catch (IndexOutOfBoundsException ex) {
                                  throw new ConcurrentModificationException();
                                  }
                                  }

                                  public void add(E e) {
                                  checkForComodification();

                                  try {
                                  int i = cursor;
                                  AbstractList.this.add(i, e);
                                  lastRet = -1;
                                  cursor = i + 1;
                                  expectedModCount = modCount;
                                  } catch (IndexOutOfBoundsException ex) {
                                  throw new ConcurrentModificationException();
                                  }
                                  }
                                  }

                                  //迭代器定义结束

                                  public List<E> subList(int fromIndex, int toIndex) {
                                  //Sublist和RandomAccessSubList在后面都作为内部类定义
                                  //这俩的分支主要是能否支持高性能随机访问,而这点在Java是依靠是否实现RandomAccess接口
                                  //来体现的,要实现接口必须得是一个类。因此,为了分歧,这里不得不创建两个类来表示两种情况。
                                  //这两个类的方法应该是大致相同的。
                                  //2
                                  return (this instanceof RandomAccess ?
                                  new RandomAccessSubList<>(this, fromIndex, toIndex) :
                                  new SubList<>(this, fromIndex, toIndex));
                                  }

                                  // Comparison and hashing

                                  public boolean equals(Object o) {
                                  if (o == this)
                                  return true;
                                  if (!(o instanceof List))
                                  return false;

                                  ListIterator<E> e1 = listIterator();
                                  ListIterator<?> e2 = ((List<?>) o).listIterator();
                                  while (e1.hasNext() && e2.hasNext()) {
                                  E o1 = e1.next();
                                  Object o2 = e2.next();
                                  //如果这东西没有重载equals方法,那此处就是单纯object对象是否相同了
                                  if (!(o1==null ? o2==null : o1.equals(o2)))
                                  return false;
                                  }
                                  //为啥size不一样不在一开始就比呢?那样不是更省花销吗
                                  return !(e1.hasNext() || e2.hasNext());
                                  }

                                  public int hashCode() {
                                  int hashCode = 1;
                                  for (E e : this)
                                  hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
                                  return hashCode;
                                  }

                                  protected void removeRange(int fromIndex, int toIndex) {
                                  ListIterator<E> it = listIterator(fromIndex);
                                  for (int i=0, n=toIndex-fromIndex; i<n; i++) {
                                  it.next();
                                  it.remove();
                                  }
                                  }

                                  //The number of times this list has been structurally modified.
                                  //3
                                  protected transient int modCount = 0;

                                  private void rangeCheckForAdd(int index) {
                                  if (index < 0 || index > size())
                                  throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
                                  }

                                  private String outOfBoundsMsg(int index) {
                                  return "Index: "+index+", Size: "+size();
                                  }
                                  }


                                  class SubList<E> extends AbstractList<E> {
                                  //这大概是对父list的引用
                                  private final AbstractList<E> l;
                                  //这大概是在父list的起始偏移量
                                  private final int offset;
                                  //sublist的长度
                                  private int size;

                                  SubList(AbstractList<E> list, int fromIndex, int toIndex) {
                                  if (fromIndex < 0)
                                  throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
                                  if (toIndex > list.size())
                                  throw new IndexOutOfBoundsException("toIndex = " + toIndex);
                                  if (fromIndex > toIndex)
                                  throw new IllegalArgumentException("fromIndex(" + fromIndex +
                                  ") > toIndex(" + toIndex + ")");
                                  l = list;
                                  offset = fromIndex;
                                  size = toIndex - fromIndex;
                                  this.modCount = l.modCount;
                                  }

                                  public E set(int index, E element) {
                                  rangeCheck(index);
                                  checkForComodification();
                                  //改变sublist也会改变父list,是否成功取决于对父list的改变是否成功
                                  return l.set(index+offset, element);
                                  }

                                  public E get(int index) {
                                  rangeCheck(index);
                                  checkForComodification();
                                  //直接取父类值
                                  return l.get(index+offset);
                                  }

                                  public int size() {
                                  checkForComodification();
                                  return size;
                                  }

                                  public void add(int index, E element) {
                                  rangeCheckForAdd(index);
                                  checkForComodification();
                                  l.add(index+offset, element);
                                  this.modCount = l.modCount;
                                  size++;
                                  }

                                  public E remove(int index) {
                                  rangeCheck(index);
                                  checkForComodification();
                                  E result = l.remove(index+offset);
                                  this.modCount = l.modCount;
                                  size--;
                                  return result;
                                  }

                                  protected void removeRange(int fromIndex, int toIndex) {
                                  checkForComodification();
                                  l.removeRange(fromIndex+offset, toIndex+offset);
                                  this.modCount = l.modCount;
                                  size -= (toIndex-fromIndex);
                                  }

                                  public boolean addAll(Collection<? extends E> c) {
                                  return addAll(size, c);
                                  }

                                  public boolean addAll(int index, Collection<? extends E> c) {
                                  rangeCheckForAdd(index);
                                  int cSize = c.size();
                                  if (cSize==0)
                                  return false;

                                  checkForComodification();
                                  l.addAll(offset+index, c);
                                  this.modCount = l.modCount;
                                  size += cSize;
                                  return true;
                                  }

                                  public Iterator<E> iterator() {
                                  return listIterator();
                                  }

                                  public ListIterator<E> listIterator(final int index) {
                                  checkForComodification();
                                  rangeCheckForAdd(index);

                                  //改变迭代器实现
                                  return new ListIterator<E>() {
                                  //子类迭代器=父类迭代器+offset
                                  private final ListIterator<E> i = l.listIterator(index+offset);

                                  public boolean hasNext() {
                                  return nextIndex() < size;
                                  }

                                  public E next() {
                                  if (hasNext())
                                  return i.next();
                                  else
                                  throw new NoSuchElementException();
                                  }

                                  public boolean hasPrevious() {
                                  return previousIndex() >= 0;
                                  }

                                  public E previous() {
                                  if (hasPrevious())
                                  return i.previous();
                                  else
                                  throw new NoSuchElementException();
                                  }

                                  public int nextIndex() {
                                  return i.nextIndex() - offset;
                                  }

                                  public int previousIndex() {
                                  return i.previousIndex() - offset;
                                  }

                                  public void remove() {
                                  i.remove();
                                  SubList.this.modCount = l.modCount;
                                  size--;
                                  }

                                  public void set(E e) {
                                  i.set(e);
                                  }

                                  public void add(E e) {
                                  i.add(e);
                                  SubList.this.modCount = l.modCount;
                                  size++;
                                  }
                                  };
                                  }

                                  //层层套娃啊
                                  public List<E> subList(int fromIndex, int toIndex) {
                                  return new SubList<>(this, fromIndex, toIndex);
                                  }

                                  private void rangeCheck(int index) {
                                  if (index < 0 || index >= size)
                                  throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
                                  }

                                  private void rangeCheckForAdd(int index) {
                                  if (index < 0 || index > size)
                                  throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
                                  }

                                  private String outOfBoundsMsg(int index) {
                                  return "Index: "+index+", Size: "+size;
                                  }

                                  private void checkForComodification() {
                                  if (this.modCount != l.modCount)
                                  throw new ConcurrentModificationException();
                                  }
                                  }

                                  //确实实现上没什么区别,主要是多了个RandomAccess的约定。
                                  class RandomAccessSubList<E> extends SubList<E> implements RandomAccess {
                                  RandomAccessSubList(AbstractList<E> list, int fromIndex, int toIndex) {
                                  super(list, fromIndex, toIndex);
                                  }

                                  public List<E> subList(int fromIndex, int toIndex) {
                                  return new RandomAccessSubList<>(this, fromIndex, toIndex);
                                  }
                                  }

                                  +
                                8. 析构

                                  +

                                  全局对象的析构同样在entry中进行:

                                  +
                                  void mini_crt_entry(void)
                                  {
                                  ...
                                  ret = main(argc,argv);
                                  exit(ret);
                                  }

                                  void exit(int exitCode)
                                  {
                                  // 执行atexit,完成所有finit钩子
                                  mini_crt_call_exit_routine();
                                  // 调用exit系统调用
                                  asm( "movl %0,%%ebx \n\t"
                                  "movl $1,%%eax \n\t"
                                  "int $0x80 \n\t"
                                  "hlt \n\t"::"m"(exitCode));
                                  }
                                  -

                                  其中:

                                    -
                                  1. 内部类ListItr和Itr的实现

                                    Itr的实现需要用到AbstratcList类的get和set方法,而显然不同Collection的get和set不一样。为了避免混淆,Itr就只能作为私有类。为了避免胡乱引用,Itr就可以直接作为内部类,共享其外部类的所有资源。

                                    -

                                    ListItr作为内部私有类很容易理解,毕竟只有list才需要它。

                                    +

                                    具体也是以链表形式管理所有的函数指针,在atexit中注册(加入链表),在mini_crt_call_exit_routine中真正调用,不多分析。

                                  2. -
                                  3. RandomAccess

                                    RandomAccess是一个空接口,它应该代表一个约定俗成的规定,即它的implementations的随机访问都是性能较高的。这个空接口思想很常见,源码带给我们的智慧。

                                    -
                                    按经验来说, a List implementation should implement this interface, if this loop:
                                    for (int i=0, n=list.size(); i < n; i++)
                                    list.get(i);

                                    runs faster than this loop:
                                    for (Iterator i=list.iterator(); i.hasNext(); )
                                    i.next();
                                  4. -
                                  5. 关于modCount字段与fail-fast机制

                                    你真的知道集合中modCount字段作用吗?

                                    -

                                    modCount字段就是保证一定程度并发安全的变量,fail-fast就是指马上抛出异常。

                                    - - -

                                    modCount不能保证绝对的并发安全,因为它只负责防范结构改变,而不负责看某位置的数据更新。

                                    -

                                    在现实中要实现对集合的边迭代边修改,下面三种方式都是错的:

                                    - - -

                                    第一种方法也可以把for里的size换成list.size()

                                    -

                                    其中乐观锁与悲观锁可见:乐观锁

                                    -
                                    -
                                      -
                                    • 乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只在更新的时候会判断一下在此期间别人有没有去更新这个数据。
                                    • -
                                    +
                                  +
                                9. +
                                +
                              2. +
                              +

                              特辑:开发中遇到的链接小问题

                                +
                              1. 已经在LD_LIBRARY_PATH中加入某个静态库的路径,但是仍然报错error while loading shared libraries: libssl.so.1.1: cannot open shared object file: No such file or directory

                                +

                                解决方法:执行sudo ldconfig

                                -

                                注意“在此期间”的含义是拿到数据到更新数据的这段时间。因为没有加锁,所以别的线程可能会更改。还有一点那就是乐观锁其实是不加锁的来保证某个变量一系列操作原子性的一种方法。

                                -
                                -
                                  -
                                • 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞,直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。
                                • -
                                +

                                ldconfig命令的作用就是将这些共享库的路径添加到动态链接器的缓存中,以便系统能够找到它们。

                                +

                                具体而言,ldconfig会检查默认的共享库路径(通常是/lib/usr/lib),以及在/etc/ld.so.conf/etc/ld.so.conf.d/目录中定义的其他路径。然后,它会更新动态链接器缓存,这样系统就知道在哪里查找共享库。

                                +

                                Q:也就是说ld不是像gcc找头文件,是根据-I选项规定的路径即时查找的,而是只根据缓存吗?所以尽管我们通过ld path环境变量设置了新查找路径,我们还是得手动刷新下ld缓存。

                                +

                                A:是的,你理解得很对。ldconfig主要是更新系统中的共享库缓存,而不是在每次程序运行时实时查找库文件的位置。动态链接器在运行时会根据缓存中的信息找到所需的共享库,以提高性能并避免在每次程序启动时重新搜索所有库。

                                +

                                当你通过LD_LIBRARY_PATH环境变量设置新的查找路径时,这只是告诉动态链接器在运行时应该额外搜索这些路径。然而,为了确保系统广泛地认识这些新的路径,以及其中的共享库,你需要运行ldconfig来更新缓存。

                            2. +
                            -

                            AbstractList帮我们实现了差不多所有方法,除了Tget(int)size()set(int, Object)add(int, E)remove(int) 。因而,接下来的两个实现中,重点关注这些就行。

                            -

                            ArrayList

                            代码

                            -

                            Implements all optional list operations, and permits all elements, including null.

                            -

                            This class is roughly equivalent to Vector, except that it is unsynchronized.

                            -

                            size(),isEmpty(),get(),set(),iterator(),listIterator()的时间复杂符是**常量级别(constant time),add()方法的时间复杂度是可变常量级别(amortized constant time),即为O(n)。大致上说,剩余方法的时间复杂度都是线性时间(linear time)。相较于LinkedList的实现,ArrayList的常量因子(constant factor)**较低。

                            -

                            An application can increase the capacity of an ArrayList instance before adding a large number of elements using the ensureCapacity operation. This may reduce the amount of incremental reallocation.

                            -

                            本身是线程不安全的,但可以通过封装类来实现线程同步。可以使用Collections.synchronizedList method.

                            -
                            List list = Collections.synchronizedList(new ArrayList(...));
                            +]]> + + books + + + + 阅读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 %}

                            -

                            总体来说实现的很多方法跟想象中差别不大,有几个比较惊艳

                            -
                            //1 cloneable
                            public class ArrayList<E> extends AbstractList<E>
                            implements List<E>, RandomAccess, Cloneable, java.io.Serializable
                            {
                            //4
                            private static final long serialVersionUID = 8683452581122892189L;

                            //可以注意一下,初始=10.
                            private static final int DEFAULT_CAPACITY = 10;

                            //Shared empty array instance used for empty instances.
                            //7
                            private static final Object[] EMPTY_ELEMENTDATA = {};

                            private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

                            //5、6
                            /*
                            The array buffer into which the elements of the ArrayList are stored.缓冲
                            ArrayList的容量=此数组的length
                            Any empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA will be expanded to DEFAULT_CAPACITY when the first element is added.
                            */
                            transient Object[] elementData; // non-private to simplify nested class access

                            private int size;

                            public ArrayList(int initialCapacity) {
                            if (initialCapacity > 0) {
                            this.elementData = new Object[initialCapacity];
                            } else if (initialCapacity == 0) {
                            this.elementData = EMPTY_ELEMENTDATA;
                            } else {
                            throw new IllegalArgumentException("Illegal Capacity: "+
                            initialCapacity);
                            }
                            }

                            public ArrayList() {
                            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
                            }

                            //in the order they are returned by c's iterator.
                            public ArrayList(Collection<? extends E> c) {
                            Object[] a = c.toArray();
                            if ((size = a.length) != 0) {
                            if (c.getClass() == ArrayList.class) {
                            elementData = a;
                            } else {
                            elementData = Arrays.copyOf(a, size, Object[].class);
                            }
                            //size==0
                            } else {
                            // replace with empty array.
                            elementData = EMPTY_ELEMENTDATA;
                            }
                            }

                            //把容量缩小为当前size。An application can use this operation to minimize the storage of an ArrayList instance.
                            public void trimToSize() {
                            //涉及List的结构性改变
                            modCount++;
                            if (size < elementData.length) {
                            elementData = (size == 0)
                            ? EMPTY_ELEMENTDATA
                            : Arrays.copyOf(elementData, size);
                            }
                            }

                            //8
                            public void ensureCapacity(int minCapacity) {
                            int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                            //如果原size!=0,除非minCapacity=0,否则必须是要扩容一次的【函数要求】,
                            //因此设置为最小值0,以确保下面的if条件一定为true。
                            ? 0
                            //如果为默认大小0,此处是必须扩为默认大小的
                            : DEFAULT_CAPACITY;

                            if (minCapacity > minExpand) {
                            ensureExplicitCapacity(minCapacity);
                            }
                            }

                            private static int calculateCapacity(Object[] elementData, int minCapacity) {
                            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                            return Math.max(DEFAULT_CAPACITY, minCapacity);
                            }
                            return minCapacity;
                            }

                            private void ensureCapacityInternal(int minCapacity) {
                            ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
                            }

                            private void ensureExplicitCapacity(int minCapacity) {
                            //修改list结构
                            //若minCapacity<elementData.length,本句modCount++始终执行。
                            modCount++;
                            // overflow-conscious code
                            if (minCapacity - elementData.length > 0)
                            grow(minCapacity);
                            }

                            private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

                            private void grow(int minCapacity) {
                            // overflow-conscious code
                            //capacity=length of elementData
                            int oldCapacity = elementData.length;
                            //每次扩1/2
                            int newCapacity = oldCapacity + (oldCapacity >> 1);
                            if (newCapacity - minCapacity < 0)
                            newCapacity = minCapacity;
                            //防止溢出
                            if (newCapacity - MAX_ARRAY_SIZE > 0)
                            newCapacity = hugeCapacity(minCapacity);
                            // minCapacity is usually close to size, so this is a win:
                            elementData = Arrays.copyOf(elementData, newCapacity);
                            }

                            private static int hugeCapacity(int minCapacity) {
                            if (minCapacity < 0) // overflow
                            throw new OutOfMemoryError();
                            return (minCapacity > MAX_ARRAY_SIZE) ?
                            Integer.MAX_VALUE :
                            MAX_ARRAY_SIZE;
                            }

                            public int size() {
                            return size;
                            }

                            public boolean isEmpty() {
                            return size == 0;
                            }

                            public boolean contains(Object o) {
                            return indexOf(o) >= 0;
                            }

                            public int indexOf(Object o) {
                            if (o == null) {
                            for (int i = 0; i < size; i++)
                            if (elementData[i]==null)
                            return i;
                            } else {
                            for (int i = 0; i < size; i++)
                            if (o.equals(elementData[i]))
                            return i;
                            }
                            return -1;
                            }

                            public int lastIndexOf(Object o) {
                            if (o == null) {
                            for (int i = size-1; i >= 0; i--)
                            if (elementData[i]==null)
                            return i;
                            } else {
                            for (int i = size-1; i >= 0; i--)
                            if (o.equals(elementData[i]))
                            return i;
                            }
                            return -1;
                            }
                            //2
                            public Object clone() {
                            try {
                            //from Object.clone():
                            //the returned object should be obtained by calling super.clone.
                            ArrayList<?> v = (ArrayList<?>) super.clone();
                            //浅拷贝,只拷贝地址
                            v.elementData = Arrays.copyOf(elementData, size);
                            v.modCount = 0;
                            return v;
                            } catch (CloneNotSupportedException e) {
                            // this shouldn't happen, since we are Cloneable
                            throw new InternalError(e);
                            }
                            }

                            public Object[] toArray() {
                            return Arrays.copyOf(elementData, size);
                            }

                            @SuppressWarnings("unchecked")
                            public <T> T[] toArray(T[] a) {
                            if (a.length < size)
                            // Make a new array of a's runtime type, but my contents:
                            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
                            System.arraycopy(elementData, 0, a, 0, size);
                            if (a.length > size)
                            a[size] = null;
                            return a;
                            }

                            // Positional Access Operations

                            @SuppressWarnings("unchecked")
                            E elementData(int index) {
                            return (E) elementData[index];
                            }

                            //这里我挺迷惑的,为什么还要再套一层elementData??
                            public E get(int index) {
                            rangeCheck(index);

                            return elementData(index);
                            }

                            public E set(int index, E element) {
                            rangeCheck(index);

                            E oldValue = elementData(index);
                            elementData[index] = element;
                            return oldValue;
                            }

                            public boolean add(E e) {
                            //既增加了modcount,也保证了capacity够用
                            ensureCapacityInternal(size + 1);
                            elementData[size++] = e;
                            return true;
                            }

                            public void add(int index, E element) {
                            rangeCheckForAdd(index);
                            ensureCapacityInternal(size + 1); // Increments modCount!!
                            System.arraycopy(elementData, index, elementData, index + 1,
                            size - index);//src dest 移动数组
                            elementData[index] = element;
                            size++;
                            }

                            public E remove(int index) {
                            rangeCheck(index);

                            modCount++;
                            E oldValue = elementData(index);

                            int numMoved = size - index - 1;
                            if (numMoved > 0)
                            System.arraycopy(elementData, index+1, elementData, index,
                            numMoved);
                            elementData[--size] = null; // clear to let GC do its work,自动清除无引用对象

                            return oldValue;
                            }

                            public boolean remove(Object o) {
                            if (o == null) {
                            for (int index = 0; index < size; index++)
                            if (elementData[index] == null) {
                            fastRemove(index);
                            return true;
                            }
                            } else {
                            for (int index = 0; index < size; index++)
                            if (o.equals(elementData[index])) {
                            fastRemove(index);
                            return true;
                            }
                            }
                            return false;
                            }

                            //这不是跟上面一模一样吗,为啥还要再写一遍?
                            private void fastRemove(int index) {
                            modCount++;
                            int numMoved = size - index - 1;
                            if (numMoved > 0)
                            System.arraycopy(elementData, index+1, elementData, index,
                            numMoved);
                            elementData[--size] = null; // clear to let GC do its work
                            }

                            public void clear() {
                            modCount++;

                            //9 我能不能直接猛一点:elementData=new Object[elementData.length]?
                            // clear to let GC do its work
                            for (int i = 0; i < size; i++)
                            elementData[i] = null;

                            size = 0;
                            }

                            public boolean addAll(Collection<? extends E> c) {
                            Object[] a = c.toArray();
                            int numNew = a.length;
                            ensureCapacityInternal(size + numNew); // Increments modCount
                            System.arraycopy(a, 0, elementData, size, numNew);
                            size += numNew;
                            return numNew != 0;
                            }

                            public boolean addAll(int index, Collection<? extends E> c) {
                            rangeCheckForAdd(index);

                            Object[] a = c.toArray();
                            int numNew = a.length;
                            ensureCapacityInternal(size + numNew); // Increments modCount

                            int numMoved = size - index;
                            if (numMoved > 0)
                            System.arraycopy(elementData, index, elementData, index + numNew,
                            numMoved);

                            System.arraycopy(a, 0, elementData, index, numNew);
                            size += numNew;
                            return numNew != 0;
                            }

                            protected void removeRange(int fromIndex, int toIndex) {
                            modCount++;
                            int numMoved = size - toIndex;
                            System.arraycopy(elementData, toIndex, elementData, fromIndex,
                            numMoved);

                            // clear to let GC do its work
                            int newSize = size - (toIndex-fromIndex);
                            for (int i = newSize; i < size; i++) {
                            elementData[i] = null;
                            }
                            size = newSize;
                            }

                            private void rangeCheck(int index) {
                            if (index >= size)
                            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
                            }

                            private void rangeCheckForAdd(int index) {
                            if (index > size || index < 0)
                            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
                            }

                            private String outOfBoundsMsg(int index) {
                            return "Index: "+index+", Size: "+size;
                            }

                            public boolean removeAll(Collection<?> c) {
                            Objects.requireNonNull(c);
                            return batchRemove(c, false);
                            }

                            public boolean retainAll(Collection<?> c) {
                            Objects.requireNonNull(c);
                            return batchRemove(c, true);
                            }

                            //这个complement做得很漂亮,兼顾实际意义又统一了代码
                            private boolean batchRemove(Collection<?> c, boolean complement) {
                            //局部变量
                            final Object[] elementData = this.elementData;
                            int r = 0, w = 0;
                            boolean modified = false;

                            try {
                            for (; r < size; r++)
                            if (c.contains(elementData[r]) == complement)
                            //原地平移,nice
                            //如果是removeall,此条件成立说明c不含有该元素,则保留该元素
                            //如果是retainall,此条件成立说明c含有该元素,则保留该元素
                            elementData[w++] = elementData[r];
                            } finally {
                            // Preserve behavioral compatibility with AbstractCollection,
                            // even if c.contains() throws.
                            if (r != size) {
                            System.arraycopy(elementData, r,
                            elementData, w,
                            size - r);
                            w += size - r;
                            }
                            if (w != size) {
                            // clear to let GC do its work
                            for (int i = w; i < size; i++)
                            elementData[i] = null;
                            modCount += size - w;
                            size = w;
                            modified = true;
                            }
                            }
                            return modified;
                            }

                            //序列化
                            private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
                            // Write out element count, and any hidden stuff
                            int expectedModCount = modCount;
                            /*from ObjectOutputStream:
                            Write the non-static and non-transient fields of the current class
                            to this stream.*/
                            s.defaultWriteObject();

                            //为啥要特地强调write size?
                            s.writeInt(size);

                            for (int i=0; i<size; i++) {
                            s.writeObject(elementData[i]);
                            }

                            //保证一定基础的序列化同步
                            if (modCount != expectedModCount) {
                            throw new ConcurrentModificationException();
                            }
                            }

                            private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
                            elementData = EMPTY_ELEMENTDATA;
                            // Read in size, and any hidden stuff
                            s.defaultReadObject();
                            //不大懂为什么这里的值被ignore了?
                            // Read in capacity
                            s.readInt(); // ignored

                            if (size > 0) {
                            // be like clone(), allocate array based upon size not capacity
                            int capacity = calculateCapacity(elementData, size);
                            SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
                            ensureCapacityInternal(size);

                            Object[] a = elementData;
                            // Read in all elements in the proper order.
                            for (int i=0; i<size; i++) {
                            a[i] = s.readObject();
                            }
                            }
                            }

                            public ListIterator<E> listIterator(int index) {
                            if (index < 0 || index > size)
                            throw new IndexOutOfBoundsException("Index: "+index);
                            return new ListItr(index);
                            }

                            public ListIterator<E> listIterator() {
                            return new ListItr(0);
                            }

                            public Iterator<E> iterator() {
                            return new Itr();
                            }

                            /**
                            * AbstractList.Itr的优化版本
                            */
                            private class Itr implements Iterator<E> {
                            int cursor; // index of next element to return
                            int lastRet = -1; // index of last element returned; -1 if no such
                            int expectedModCount = modCount;

                            Itr() {}

                            public boolean hasNext() {
                            return cursor != size;
                            }

                            @SuppressWarnings("unchecked")
                            public E next() {
                            checkForComodification();
                            int i = cursor;
                            if (i >= size)
                            throw new NoSuchElementException();
                            //内部类访问外部类this的方法
                            Object[] elementData = ArrayList.this.elementData;
                            if (i >= elementData.length)
                            throw new ConcurrentModificationException();
                            cursor = i + 1;
                            return (E) elementData[lastRet = i];
                            }

                            public void remove() {
                            if (lastRet < 0)
                            throw new IllegalStateException();
                            checkForComodification();

                            try {
                            //change here
                            //确实AbstractList的那个remove应该更适用于LinkedList
                            ArrayList.this.remove(lastRet);
                            cursor = lastRet;
                            lastRet = -1;
                            expectedModCount = modCount;
                            } catch (IndexOutOfBoundsException ex) {
                            throw new ConcurrentModificationException();
                            }
                            }

                            @Override
                            @SuppressWarnings("unchecked")
                            public void forEachRemaining(Consumer<? super E> consumer) {
                            Objects.requireNonNull(consumer);
                            final int size = ArrayList.this.size;
                            int i = cursor;
                            //这里不抛异常吗
                            if (i >= size) {
                            return;
                            }
                            final Object[] elementData = ArrayList.this.elementData;
                            //好像确实是并发修改了,毕竟上面已经test过i<size<capacity了
                            if (i >= elementData.length) {
                            throw new ConcurrentModificationException();
                            }
                            while (i != size && modCount == expectedModCount) {
                            consumer.accept((E) elementData[i++]);
                            }
                            // update once at end of iteration to reduce heap write traffic
                            //10
                            cursor = i;
                            lastRet = i - 1;
                            checkForComodification();
                            }

                            final void checkForComodification() {
                            if (modCount != expectedModCount)
                            throw new ConcurrentModificationException();
                            }
                            }

                            /**
                            * AbstractList.ListItr的优化版本
                            */
                            private class ListItr extends Itr implements ListIterator<E> {
                            ListItr(int index) {
                            super();
                            cursor = index;
                            }

                            public boolean hasPrevious() {
                            return cursor != 0;
                            }

                            public int nextIndex() {
                            return cursor;
                            }

                            public int previousIndex() {
                            return cursor - 1;
                            }

                            @SuppressWarnings("unchecked")
                            public E previous() {
                            checkForComodification();
                            int i = cursor - 1;
                            if (i < 0)
                            throw new NoSuchElementException();
                            Object[] elementData = ArrayList.this.elementData;
                            //11 防止并发修改,比如在这期间进行了trim
                            if (i >= elementData.length)
                            throw new ConcurrentModificationException();
                            cursor = i;
                            return (E) elementData[lastRet = i];
                            }

                            public void set(E e) {
                            if (lastRet < 0)
                            throw new IllegalStateException();
                            checkForComodification();

                            try {
                            ArrayList.this.set(lastRet, e);
                            } catch (IndexOutOfBoundsException ex) {
                            throw new ConcurrentModificationException();
                            }
                            }

                            public void add(E e) {
                            checkForComodification();

                            try {
                            int i = cursor;
                            ArrayList.this.add(i, e);
                            cursor = i + 1;
                            lastRet = -1;
                            expectedModCount = modCount;
                            } catch (IndexOutOfBoundsException ex) {
                            throw new ConcurrentModificationException();
                            }
                            }
                            }

                            public List<E> subList(int fromIndex, int toIndex) {
                            subListRangeCheck(fromIndex, toIndex, size);
                            return new SubList(this, 0, fromIndex, toIndex);
                            }

                            static void subListRangeCheck(int fromIndex, int toIndex, int size) {
                            if (fromIndex < 0)
                            throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
                            if (toIndex > size)
                            throw new IndexOutOfBoundsException("toIndex = " + toIndex);
                            if (fromIndex > toIndex)
                            throw new IllegalArgumentException("fromIndex(" + fromIndex +
                            ") > toIndex(" + toIndex + ")");
                            }

                            //依然内部类。不过这里应该用了个作用域的性质。相比于AbstractList定义在List类外部的
                            //Sublist,应该会更优先使用定义在内部的Sublist
                            //12 注意,这里的sublist没有直接extends ArrayList
                            private class SubList extends AbstractList<E> implements RandomAccess {
                            private final AbstractList<E> parent;
                            //新增成员
                            private final int parentOffset;
                            private final int offset;
                            int size;

                            SubList(AbstractList<E> parent,
                            int offset, int fromIndex, int toIndex) {
                            this.parent = parent;
                            //这个parentOffset应该是指相较于最近的父亲的偏移量,offset应该就是相较于最底层的父亲的偏移量
                            this.parentOffset = fromIndex;
                            this.offset = offset + fromIndex;
                            this.size = toIndex - fromIndex;
                            this.modCount = ArrayList.this.modCount;
                            }

                            public E set(int index, E e) {
                            rangeCheck(index);
                            checkForComodification();
                            E oldValue = ArrayList.this.elementData(offset + index);
                            ArrayList.this.elementData[offset + index] = e;
                            return oldValue;
                            }

                            public E get(int index) {
                            rangeCheck(index);
                            checkForComodification();
                            return ArrayList.this.elementData(offset + index);
                            }

                            public int size() {
                            checkForComodification();
                            return this.size;
                            }

                            public void add(int index, E e) {
                            rangeCheckForAdd(index);
                            checkForComodification();
                            //13
                            parent.add(parentOffset + index, e);
                            this.modCount = parent.modCount;
                            this.size++;
                            }

                            public E remove(int index) {
                            rangeCheck(index);
                            checkForComodification();
                            E result = parent.remove(parentOffset + index);
                            this.modCount = parent.modCount;
                            this.size--;
                            return result;
                            }

                            protected void removeRange(int fromIndex, int toIndex) {
                            checkForComodification();
                            parent.removeRange(parentOffset + fromIndex,
                            parentOffset + toIndex);
                            this.modCount = parent.modCount;
                            this.size -= toIndex - fromIndex;
                            }

                            public boolean addAll(Collection<? extends E> c) {
                            return addAll(this.size, c);
                            }

                            public boolean addAll(int index, Collection<? extends E> c) {
                            rangeCheckForAdd(index);
                            int cSize = c.size();
                            if (cSize==0)
                            return false;

                            checkForComodification();
                            parent.addAll(parentOffset + index, c);
                            this.modCount = parent.modCount;
                            this.size += cSize;
                            return true;
                            }

                            public Iterator<E> iterator() {
                            return listIterator();
                            }

                            public ListIterator<E> listIterator(final int index) {
                            checkForComodification();
                            rangeCheckForAdd(index);
                            final int offset = this.offset;

                            return new ListIterator<E>() {
                            int cursor = index;
                            int lastRet = -1;
                            int expectedModCount = ArrayList.this.modCount;

                            public boolean hasNext() {
                            //内部内部类还可以访问外部类和外部外部类
                            return cursor != SubList.this.size;
                            }

                            @SuppressWarnings("unchecked")
                            public E next() {
                            checkForComodification();
                            int i = cursor;
                            if (i >= SubList.this.size)
                            throw new NoSuchElementException();
                            Object[] elementData = ArrayList.this.elementData;
                            if (offset + i >= elementData.length)
                            throw new ConcurrentModificationException();
                            cursor = i + 1;
                            return (E) elementData[offset + (lastRet = i)];
                            }

                            public boolean hasPrevious() {
                            return cursor != 0;
                            }

                            @SuppressWarnings("unchecked")
                            public E previous() {
                            checkForComodification();
                            int i = cursor - 1;
                            if (i < 0)
                            throw new NoSuchElementException();
                            Object[] elementData = ArrayList.this.elementData;
                            if (offset + i >= elementData.length)
                            throw new ConcurrentModificationException();
                            cursor = i;
                            return (E) elementData[offset + (lastRet = i)];
                            }

                            @SuppressWarnings("unchecked")
                            public void forEachRemaining(Consumer<? super E> consumer) {
                            Objects.requireNonNull(consumer);
                            final int size = SubList.this.size;
                            int i = cursor;
                            if (i >= size) {
                            return;
                            }
                            final Object[] elementData = ArrayList.this.elementData;
                            if (offset + i >= elementData.length) {
                            throw new ConcurrentModificationException();
                            }
                            while (i != size && modCount == expectedModCount) {
                            consumer.accept((E) elementData[offset + (i++)]);
                            }
                            // update once at end of iteration to reduce heap write traffic
                            lastRet = cursor = i;
                            checkForComodification();
                            }

                            public int nextIndex() {
                            return cursor;
                            }

                            public int previousIndex() {
                            return cursor - 1;
                            }

                            public void remove() {
                            if (lastRet < 0)
                            throw new IllegalStateException();
                            checkForComodification();

                            try {
                            SubList.this.remove(lastRet);
                            cursor = lastRet;
                            lastRet = -1;
                            expectedModCount = ArrayList.this.modCount;
                            } catch (IndexOutOfBoundsException ex) {
                            //确实要是IndexOutOfBounds的话,就说明lastRet改了,说明Concurrent了
                            throw new ConcurrentModificationException();
                            }
                            }

                            public void set(E e) {
                            if (lastRet < 0)
                            throw new IllegalStateException();
                            checkForComodification();

                            try {
                            ArrayList.this.set(offset + lastRet, e);
                            } catch (IndexOutOfBoundsException ex) {
                            throw new ConcurrentModificationException();
                            }
                            }

                            public void add(E e) {
                            checkForComodification();

                            try {
                            int i = cursor;
                            SubList.this.add(i, e);
                            cursor = i + 1;
                            lastRet = -1;
                            expectedModCount = ArrayList.this.modCount;
                            } catch (IndexOutOfBoundsException ex) {
                            throw new ConcurrentModificationException();
                            }
                            }

                            final void checkForComodification() {
                            if (expectedModCount != ArrayList.this.modCount)
                            throw new ConcurrentModificationException();
                            }
                            };
                            }

                            public List<E> subList(int fromIndex, int toIndex) {
                            subListRangeCheck(fromIndex, toIndex, size);
                            return new SubList(this, offset, fromIndex, toIndex);
                            }

                            public Spliterator<E> spliterator() {
                            checkForComodification();
                            return new ArrayListSpliterator<E>(ArrayList.this, offset,
                            offset + this.size, this.modCount);
                            }
                            }

                            @Override
                            public void forEach(Consumer<? super E> action) {
                            Objects.requireNonNull(action);
                            final int expectedModCount = modCount;
                            @SuppressWarnings("unchecked")
                            final E[] elementData = (E[]) this.elementData;
                            final int size = this.size;
                            //一修改就会寄
                            for (int i=0; modCount == expectedModCount && i < size; i++) {
                            action.accept(elementData[i]);
                            }
                            if (modCount != expectedModCount) {
                            throw new ConcurrentModificationException();
                            }
                            }

                            @Override
                            public Spliterator<E> spliterator() {
                            return new ArrayListSpliterator<>(this, 0, -1, 0);
                            }

                            /** Index-based split-by-two, lazily initialized Spliterator */
                            //基于索引的二分法,懒加载的 Spliterator
                            static final class ArrayListSpliterator<E> implements Spliterator<E> {...}

                            @Override
                            public boolean removeIf(Predicate<? super E> filter) {
                            Objects.requireNonNull(filter);

                            int removeCount = 0;
                            //666,用了类似掩码的思想,这样就能避免多次移动数组了,实现O(n),很不错
                            final BitSet removeSet = new BitSet(size);
                            final int expectedModCount = modCount;
                            final int size = this.size;
                            for (int i=0; modCount == expectedModCount && i < size; i++) {
                            @SuppressWarnings("unchecked")
                            final E element = (E) elementData[i];
                            if (filter.test(element)) {
                            //set:Sets the bit at the specified index to true.
                            removeSet.set(i);
                            removeCount++;
                            }
                            }
                            if (modCount != expectedModCount) {
                            throw new ConcurrentModificationException();
                            }

                            // shift surviving elements left over the spaces left by removed elements
                            final boolean anyToRemove = removeCount > 0;
                            if (anyToRemove) {
                            final int newSize = size - removeCount;
                            for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
                            //nextClearBit:Returns the index of the first bit that is set to false
                            //false表示不移走,true表示移走
                            i = removeSet.nextClearBit(i);
                            elementData[j] = elementData[i];
                            }
                            for (int k=newSize; k < size; k++) {
                            elementData[k] = null; // Let gc do its work
                            }
                            this.size = newSize;
                            if (modCount != expectedModCount) {
                            throw new ConcurrentModificationException();
                            }
                            modCount++;
                            }

                            return anyToRemove;
                            }

                            @Override
                            @SuppressWarnings("unchecked")
                            public void replaceAll(UnaryOperator<E> operator) {
                            Objects.requireNonNull(operator);
                            final int expectedModCount = modCount;
                            final int size = this.size;
                            for (int i=0; modCount == expectedModCount && i < size; i++) {
                            elementData[i] = operator.apply((E) elementData[i]);
                            }
                            if (modCount != expectedModCount) {
                            throw new ConcurrentModificationException();
                            }
                            modCount++;
                            }

                            @Override
                            @SuppressWarnings("unchecked")
                            public void sort(Comparator<? super E> c) {
                            final int expectedModCount = modCount;
                            Arrays.sort((E[]) elementData, 0, size, c);
                            if (modCount != expectedModCount) {
                            throw new ConcurrentModificationException();
                            }
                            modCount++;
                            }
                            }
                            - -

                            其中:

                              -
                            1. Cloneable接口

                              与RandomAccess一样,都是一个规定性质的接口。

                              -
                              -

                              A class implements the Cloneable interface to indicate to the Object.clone() method that it is legal for that method to make a field-for-field copy of instances of that class.

                              -

                              Classes that implement this interface should override Object.clone (which is protected) with a public method.

                              +

                              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的那个是一样的】

                              -
                            2. -
                            3. clone与浅拷贝(shallow copy)
                                  public static void main(String[] args) {
                              ArrayList<Student> arr = new ArrayList<>();
                              arr.add(new Student("lylt",15));
                              ArrayList<Student> cl = (ArrayList)arr.clone();
                              System.out.println(cl.get(0).toString());
                              cl.get(0).name="Sam";
                              System.out.println(cl.get(0).toString());
                              System.out.println(arr.get(0).toString());
                              cl.set(0,new Student("HWX",19));
                              System.out.println(cl.get(0).toString());
                              System.out.println(arr.get(0).toString());
                              }
                              /*运行结果:
                              name :lyltage :15
                              name :Samage :15
                              name :Samage :15
                              name :HWXage :19
                              name :Samage :15
                              */
                              +

                              map没有迭代器

                              + -

                              结合内部代码可知,确实跟上面那个toArray的原理是一样的,只拷贝地址。

                              -

                              clone是浅拷贝。

                              -

                              浅拷贝与深拷贝的区别

                              -
                            4. -
                            5. 关于子类继承到的父类内部类

                              本来在犹豫,子类默认继承到的内部类里面用到的外部类方法的版本是取父还是取子,经过以下实验可知,是取能访问到的最新版本。

                              -
                              public class Main {
                              public static void main(String[] args) {
                              ChildOuter chldot = new ChildOuter();
                              chldot.printname();
                              chldot.in.printall();
                              }
                              }
                              /*结果:子类声明为private的成员字段不能被从父类继承而来的方法访问到
                              Father
                              I am father!*/
                              class FatherOuter{
                              String name="Father";
                              private void print(){
                              System.out.println("I am father!");
                              }
                              public Inner in = new Inner();
                              public class Inner{
                              public int haha=1;
                              void printall(){
                              print();
                              }
                              }
                              public void printname(){
                              System.out.println(name);
                              }
                              }

                              class ChildOuter extends FatherOuter{
                              private String name="Child";
                              private void print(){System.out.println("I am child!");}
                              }
                              +

                              代码:

                              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 迭代
                              }
                              -

                              如若把Father和Child内的print类都换成public:

                              -
                              class FatherOuter{
                              //...
                              public void print(){
                              System.out.println("I am father!");
                              }
                              //...
                              }

                              class ChildOuter extends FatherOuter{
                              //...
                              public void print(){System.out.println("I am child!");}
                              }
                              /*结果:访问最新版本
                              I am child!*/
                              +

                              其中:

                                +
                              1. get return null时的情况

                                get return null when value==NULL or key不存在。

                                +

                                为了区分这两种情况,写代码时可以用:

                                +
                                if !containsKey(key){
                                key不存在
                                }
                                Obj obj=get(key);
                                -
                                //结论:内部类可以访问外部类所有不论私有还是公有的资源;会优先访问最新版本【父子类而言】
                                //子类声明为private的成员字段不能被从父类继承而来的方法访问到,只会访问能访问到的最新版本
                              2. -
                              3. 序列化versionID

                                ArrayList实现了java.io.Serializable接口,故而可以被序列化和反序列化,就需要有个序列化版本ID

                                -

                                What is a serialVersionUID and why should I use it?

                                -
                                -

                                这东西是用来在反序列化的时候保证收到的对象和发送的对象的类是相同的。If the receiver has loaded a class for the object that has a different serialVersionUID than that of the corresponding sender’s class, then deserialization will result in an InvalidClassException.

                                -

                                The field serialVersionUID must be static, final, and of type long.

                                -

                                If a serializable class does not explicitly declare a serialVersionUID, then the serialization runtime will calculate a default serialVersionUID value for that class based on various aspects of the class, as described in the Java(TM) Object Serialization Specification.

                                +

                                其实源码中的getordefault方法就给出了应用典范

                                +
                                default V getOrDefault(Object key, V defaultValue) {
                                V v;
                                return (((v = get(key)) != null) || containsKey(key))
                                ? v
                                : defaultValue;
                                }
                              4. +
                              5. 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进行遍历。

                              6. -
                              7. transient

                                Java中transient关键字的详细总结

                                -
                                -

                                transient是短暂的意思。对于transient 修饰的成员变量,在类的实例对象的序列化处理过程中会被忽略。 因此,transient变量不会贯穿对象的序列化和反序列化,生命周期仅存于调用者的内存中而不会写到磁盘里进行持久化。

                                -

                                在持久化对象时,对于一些特殊的数据成员(如用户的密码,银行卡号等),我们不想用序列化机制来保存它。为了在一个特定对象的一个成员变量上关闭序列化,可以在这个成员变量前加上关键字transient。

                                -

                                注意static修饰的静态变量天然就是不可序列化的。一个静态变量不管是否被transient修饰,均不能被序列化(如果反序列化后类中static变量还有值,则值为当前JVM中对应static变量的值)。序列化保存的是对象状态,静态变量保存的是类状态,因此序列化并不保存静态变量。

                                -

                                使用场景
                                (1)类中的字段值可以根据其它字段推导出来,如一个长方形类有三个属性长度、宽度、面积,面积不需要序列化。
                                (2) 一些安全性的信息,一般情况下是不能离开JVM的。
                                (3)如果类中使用了Logger实例,那么Logger实例也是不需要序列化的

                                +
                              8. 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
                                -

                                但其实还有一个问题,它这边源码对这个transient的注释是:non-private to simplify nested class access,“非私有以简化嵌套类访问”。问题是这个transient和类的公有还是私有有什么关系呢?

                                -

                                Why is the data array in java.util.ArrayList package-private?

                                -

                                其实没怎么看懂23333

                                -
                              9. -
                              10. ArrayList的elementData虽然被transient修饰,但仍然能够序列化

                                ArrayList中elementData为何被transient修饰?

                                -
                              11. -
                              12. 关于static的空数组

                                EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA这两个空数组可用于表示两种情况:

                                -

                                new ArrayList(0) ->EMPTY_ELEMENTDATA

                                -

                                new ArrayList() ->DEFAULTCAPACITY_EMPTY_ELEMENTDATA

                                -

                                之所以用静态,是提取了共性:不论是需要什么ArrayList,其空形态不都一样吗(

                                -

                                这样可以避免了制造对象的浪费。very good。

                                +

                                相比于其它的view,多了第二句话

                              13. -
                              14. 关于扩容的连环计

                                我其实觉得不必写那么麻烦……

                                -

                                In the JAVA ArrayList source code, why does array expansion should be divided into ensureCapacityInternal and ensureCapacity two sides?

                                -

                                经过测试替换可得,确实可以像我那样写。

                                -
                                		//此处的ArrayList是魔改过的
                                //now the capacity is 0
                                ArrayList a = new ArrayList<>();
                                System.out.println(a.getCapacity());
                                //扩容到DEFAULT
                                a.add(new Student("Lily",15));
                                System.out.println(a.getCapacity());
                                addStudent(a);
                                System.out.println(a.getCapacity());
                                /*运行结果:
                                0
                                10
                                33
                                */
                              15. -
                              16. 关于clear我的写法

                                In the Java ArrayList source code, in the clear function, can my rewrite more efficient than the origin source code?

                                -
                                ArrayList a = new ArrayList<>();
                                addStudent(a);
                                long sum1=0;
                                for (int i=0;i<50000;i++){
                                long startTime = System.currentTimeMillis();
                                a.clear1();
                                addStudent(a);
                                long endTime = System.currentTimeMillis();
                                sum1+=endTime-startTime;
                                }
                                //sum1/=500;
                                System.out.println("clear1: "+sum1);
                                sum1=0;
                                for (int i=0;i<50000;i++){
                                long startTime = System.currentTimeMillis();
                                a.clear2();
                                addStudent(a);
                                long endTime = System.currentTimeMillis();
                                sum1+=endTime-startTime;
                                }
                                //sum1/=500;
                                System.out.println("clear2: "+sum1);
                                +
                              17. 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)));
                                -

                                经测试发现不分伯仲()也确实差距应该很小2333

                                -

                                不过依照一个回答:

                                +

                                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

                                -

                                Your approach explicitly throws the backing array away. The existing implementation attempts to reuse it. So even if your approach is faster in isolation, in practice it will almost certainly be less performant. Since calling clear() is a sign you intend to reuse the ArrayList.

                                +

                                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.

                                -

                                其实感觉我的clear说不定花销更大,毕竟要创建一个新对象(

                                -
                              18. -
                              19. heap write traffic

                                What is heap write traffic and why it is required in ArrayList?

                                -

                                详见第二个答案。

                                - - -

                                栈放在一级缓存,堆放在二级缓存

                                -

                                总之意思就是成员变量写在堆里,局部变量写在栈里,做

                                -
                                while (i != size && modCount == expectedModCount) {
                                consumer.accept((E) elementData[i++]);
                                // Update cursor while iterating
                                cursor = i;
                                }
                                - -

                                比做

                                -
                                while (cursor != size && modCount == expectedModCount) {
                                consumer.accept((E) elementData[cursor++]);
                                }
                                - -

                                花销更小

                                -
                              20. -
                              21. if (i >= elementData.length)

                                is i >= elementData.length in ArrayList::iterator redundant?

                                -
                                public E previous() {
                                checkForComodification();
                                int i = cursor - 1;
                                if (i < 0)
                                throw new NoSuchElementException();
                                Object[] elementData = ArrayList.this.elementData;
                                if (i >= elementData.length)
                                throw new ConcurrentModificationException();
                                cursor = i;
                                return (E) elementData[lastRet = i];
                                }
                                +
                                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}}
                                -

                                如果 user invoke trimToSize method ,就会导致在checkForComodification();if (i >= elementData.length)之间发生ArrayIndexOutOfBounds。而在if (i >= elementData.length)之后trim没有影响,因为我们的局部变量已经保存了原来的elementData,此时再trim只是修改成员变量的elementData,局部变量依然不变。

                                +

                                computIfAbsent发现此时map里面没有这个“line”key,就执行第二个参数的lambda表达式,把一个new TreeMap<>以line为关键字放入,并且返回该TreeMap。

                              22. -
                              23. sublist不可序列化,且not cloneable
                                private class SubList extends AbstractList<E> implements RandomAccess 
                                +
                              24. merge

                                +

                                看起来非常实用:

                                +

                                This method may be of use when combining multiple mapped values for a key【相同key不同value合并】. For example, to either create or append a String msg to a value mapping:

                                +
                                map.merge(key, msg, String::concat)
                                -

                                sublist没有extends Cloneable, java.io.Serializable这两个接口

                                +

                                所举代码段意为把新值通过字符串拼接接在旧值后面。

                                +

                                应该也可以用于集合合并。总之具体实现方法取决于传入的function参数,非常实用

                                +
                              25. -
                              26. parent和ArrayList.this

                                首先,这两个是同一个吗?其次,这俩是否是同一个跟sub的级数有关系吗,就比如一级sub都是同一个,多级sub就不是同一个了?

                                -

                                经过对ArrayList的一些public和以下代码的测试,得出结论:这两个只有在第一级sub的时候是同一个。parent指向直系父亲,ArrayList.this指向root父亲。

                                -
                                //In ArrayList:
                                public final AbstractList<E> parent;
                                public ArrayList getRoot(){return ArrayList.this;}
                                //In test main:
                                public static void main(String[] args) {
                                ArrayList a=new ArrayList();
                                addStudent(a);
                                System.out.println(a.hashCode());

                                ArrayList.SubList suba= (ArrayList.SubList) a.subList(1,7);
                                System.out.println(suba.getRoot().hashCode()+"\t"+suba.parent.hashCode());
                                System.out.println(suba.hashCode());

                                ArrayList.SubList subsuba=(ArrayList.SubList) suba.subList(2,4);
                                System.out.println(subsuba.getRoot().hashCode()+"\t"+subsuba.parent.hashCode());
                                System.out.println(subsuba.hashCode());

                                ArrayList.SubList subsubsuba=(ArrayList.SubList) subsuba.subList(0,1);
                                System.out.println(subsubsuba.getRoot().hashCode()+"\t"+subsubsuba.parent.hashCode());
                                }

                                /*输出结果:
                                779301330
                                779301330 779301330
                                -954172011
                                779301330 -954172011
                                -95519366
                                779301330 -95519366
                                */
                                - -

                                总之,ArrayList的sublist实现方式相当于串成了一条父子继承串,多级sub,至于这么干相比原来的只有两级父子关系的方法好在哪就不知道了

                                +
                              27. 迭代器

                                map本身没有迭代器。

                                +

                                因而在对map进行遍历时,只能通过其keyset、valueset以及entryset来实现。

                                +

                                具体详见:HashMap的四种遍历方式

                              -

                              AbstractSequentialList(A)

                              -

                              提供顺序访问list的基本骨架

                              -

                              To implement a list the programmer needs only to extend this class and provide implementations for the listIterator and size methods. For an unmodifiable list, the programmer need only implement the list iterator’s hasNext, next, hasPrevious, previous and index methods.
                              For a modifiable list the programmer should additionally implement the list iterator’s set method. For a variable-size list the programmer should additionally implement the list iterator’s remove and add methods.

                              +

                              AbstractMap

                              +

                              To implement an unmodifiable map, the programmer needs only to extend this class and provide an implementation for the entrySet method, which returns a set-view of the map’s mappings. Typically, the returned set will, in turn, be implemented atop AbstractSet. This set should not support the add or remove methods, and its iterator should not support the remove method.

                              +

                              To implement a modifiable map, the programmer must additionally override this class’s put method (which otherwise throws an UnsupportedOperationException), and the iterator returned by entrySet().iterator() must additionally implement its remove method.

                              -

                              代码:

                              public abstract class AbstractSequentialList<E> extends AbstractList<E> {

                              protected AbstractSequentialList() {
                              }

                              public E get(int index) {
                              try {
                              //1
                              return listIterator(index).next();
                              } catch (NoSuchElementException exc) {
                              throw new IndexOutOfBoundsException("Index: "+index);
                              }
                              }

                              public E set(int index, E element) {
                              try {
                              ListIterator<E> e = listIterator(index);
                              E oldVal = e.next();
                              e.set(element);
                              return oldVal;
                              } catch (NoSuchElementException exc) {
                              throw new IndexOutOfBoundsException("Index: "+index);
                              }
                              }

                              public void add(int index, E element) {
                              try {
                              listIterator(index).add(element);
                              } catch (NoSuchElementException exc) {
                              throw new IndexOutOfBoundsException("Index: "+index);
                              }
                              }

                              public E remove(int index) {
                              try {
                              ListIterator<E> e = listIterator(index);
                              E outCast = e.next();
                              e.remove();
                              return outCast;
                              } catch (NoSuchElementException exc) {
                              throw new IndexOutOfBoundsException("Index: "+index);
                              }
                              }
                              // Bulk Operations

                              public boolean addAll(int index, Collection<? extends E> c) {
                              try {
                              boolean modified = false;
                              ListIterator<E> e1 = listIterator(index);
                              Iterator<? extends E> e2 = c.iterator();
                              while (e2.hasNext()) {
                              e1.add(e2.next());
                              modified = true;
                              }
                              return modified;
                              } catch (NoSuchElementException exc) {
                              throw new IndexOutOfBoundsException("Index: "+index);
                              }
                              }
                              // Iterators

                              public Iterator<E> iterator() {
                              return listIterator();
                              }

                              public abstract ListIterator<E> listIterator(int index);
                              }
                              + -

                              其中:

                                -
                              1. get和set方法通过Iterator实现

                                随机访问的AbstractList的iterator的方法借助了主类的get和set,跟这里正好反过来。但注意哈,下面的LinkedList实现把以上差不多所有的方法都重写了,因而get和set之类的方法,LinkedList并不是依靠迭代器的。

                                +

                                最核心的还是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不需要这个性质,因此作为成员变量花费更小

                                -

                                LinkedList

                                -

                                双向链表,实现List和Deque

                                -

                                并发不安全。List list = Collections.synchronizedList(new LinkedList(…));

                                -

                                印象:漂亮的指针操作,以及好像很少抛出异常,还有很多很多繁琐的方法(

                                +

                                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等会看看

                                -

                                代码:

                                public class LinkedList<E>
                                extends AbstractSequentialList<E>
                                implements List<E>, Deque<E>, Cloneable, java.io.Serializable
                                {
                                //怎么连size也是transient?这不是代表着该对象的信息吗(
                                transient int size = 0;

                                transient Node<E> first;

                                transient Node<E> last;

                                public LinkedList() {
                                }

                                public LinkedList(Collection<? extends E> c) {
                                //调用空构造器
                                this();
                                addAll(c);
                                }

                                //把e接在链表头
                                private void linkFirst(E e) {
                                final Node<E> f = first;
                                final Node<E> newNode = new Node<>(null, e, f);
                                first = newNode;
                                if (f == null)
                                last = newNode;
                                else
                                f.prev = newNode;
                                size++;
                                modCount++;
                                }

                                void linkLast(E e) {
                                final Node<E> l = last;
                                final Node<E> newNode = new Node<>(l, e, null);
                                last = newNode;
                                if (l == null)
                                first = newNode;
                                else
                                l.next = newNode;
                                size++;
                                modCount++;
                                }

                                // Inserts element e before non-null Node succ.
                                void linkBefore(E e, Node<E> succ) {
                                // assert succ != null;
                                final Node<E> pred = succ.prev;
                                final Node<E> newNode = new Node<>(pred, e, succ);
                                succ.prev = newNode;
                                if (pred == null)
                                first = newNode;
                                else
                                pred.next = newNode;
                                size++;
                                modCount++;
                                }

                                private E unlinkFirst(Node<E> f) {
                                // assert f == first && f != null;
                                //这里就只靠注释会不会危险了()不过也确实会自动帮我们抛出NullPointerException的
                                //而且我在想,不是first已经指向头结点了吗,那你为什么还要把头结点作为参数传进来。。。
                                final E element = f.item;
                                final Node<E> next = f.next;
                                f.item = null;
                                f.next = null; // help GC
                                first = next;
                                if (next == null)
                                last = null;
                                else
                                next.prev = null;
                                size--;
                                modCount++;
                                return element;
                                }

                                //3
                                private E unlinkLast(Node<E> l) {
                                // assert l == last && l != null;
                                final E element = l.item;
                                final Node<E> prev = l.prev;
                                l.item = null;
                                l.prev = null; // help GC
                                last = prev;
                                if (prev == null)
                                first = null;
                                else
                                prev.next = null;
                                size--;
                                modCount++;
                                return element;
                                }

                                E unlink(Node<E> x) {
                                // assert x != null;
                                final E element = x.item;
                                final Node<E> next = x.next;
                                final Node<E> prev = x.prev;

                                if (prev == null) {
                                first = next;
                                } else {
                                prev.next = next;
                                x.prev = null;
                                }

                                if (next == null) {
                                last = prev;
                                } else {
                                next.prev = prev;
                                x.next = null;
                                }

                                x.item = null;
                                size--;
                                modCount++;
                                return element;
                                }

                                public E getFirst() {
                                final Node<E> f = first;
                                if (f == null)
                                throw new NoSuchElementException();
                                return f.item;
                                }

                                public E getLast() {
                                final Node<E> l = last;
                                if (l == null)
                                throw new NoSuchElementException();
                                return l.item;
                                }

                                public E removeFirst() {
                                final Node<E> f = first;
                                if (f == null)
                                throw new NoSuchElementException();
                                return unlinkFirst(f);
                                }

                                public E removeLast() {
                                final Node<E> l = last;
                                if (l == null)
                                throw new NoSuchElementException();
                                return unlinkLast(l);
                                }

                                public void addFirst(E e) {linkFirst(e);}

                                public void addLast(E e) {linkLast(e);}

                                public boolean contains(Object o) {return indexOf(o) != -1;}

                                public int size() {return size;}

                                public boolean add(E e) {
                                linkLast(e);
                                return true;
                                }

                                public boolean remove(Object o) {
                                if (o == null) {
                                for (Node<E> x = first; x != null; x = x.next) {
                                if (x.item == null) {
                                unlink(x);
                                return true;
                                }
                                }
                                } else {
                                for (Node<E> x = first; x != null; x = x.next) {
                                if (o.equals(x.item)) {
                                unlink(x);
                                return true;
                                }
                                }
                                }
                                return false;
                                }

                                public boolean addAll(Collection<? extends E> c) {
                                return addAll(size, c);
                                }

                                public boolean addAll(int index, Collection<? extends E> c) {
                                checkPositionIndex(index);

                                Object[] a = c.toArray();
                                int numNew = a.length;
                                if (numNew == 0)
                                return false;

                                //分成两段
                                Node<E> pred, succ;
                                if (index == size) {
                                succ = null;
                                pred = last;
                                } else {
                                succ = node(index);
                                pred = succ.prev;
                                }

                                //往中间加料
                                for (Object o : a) {
                                @SuppressWarnings("unchecked") E e = (E) o;
                                Node<E> newNode = new Node<>(pred, e, null);
                                if (pred == null)
                                first = newNode;
                                else
                                pred.next = newNode;
                                pred = newNode;
                                }

                                //合起来
                                if (succ == null) {
                                last = pred;
                                } else {
                                pred.next = succ;
                                succ.prev = pred;
                                }

                                size += numNew;
                                modCount++;
                                return true;
                                }


                                public void clear() {
                                // 1
                                // Clearing all of the links between nodes is "unnecessary", but:
                                // - helps a generational GC if the discarded nodes inhabit
                                // more than one generation
                                // - is sure to free memory even if there is a reachable Iterator
                                for (Node<E> x = first; x != null; ) {
                                Node<E> next = x.next;
                                x.item = null;
                                x.next = null;
                                x.prev = null;
                                x = next;
                                }
                                first = last = null;
                                size = 0;
                                modCount++;
                                }

                                // Positional Access Operations

                                public E get(int index) {
                                checkElementIndex(index);
                                return node(index).item;
                                }

                                public E set(int index, E element) {
                                checkElementIndex(index);
                                Node<E> x = node(index);
                                E oldVal = x.item;
                                x.item = element;
                                return oldVal;
                                }

                                public void add(int index, E element) {
                                checkPositionIndex(index);

                                if (index == size)
                                linkLast(element);
                                else
                                linkBefore(element, node(index));
                                }

                                public E remove(int index) {
                                checkElementIndex(index);
                                return unlink(node(index));
                                }

                                Node<E> node(int index) {
                                // assert isElementIndex(index);
                                //所以为啥不能check一下?

                                //做了个小小的优化,如果在前半就从开头找,在后半就从最后往前找
                                if (index < (size >> 1)) {
                                Node<E> x = first;
                                for (int i = 0; i < index; i++)
                                x = x.next;
                                return x;
                                } else {
                                Node<E> x = last;
                                for (int i = size - 1; i > index; i--)
                                x = x.prev;
                                return x;
                                }
                                }

                                // Search Operations

                                public int indexOf(Object o) {...}

                                public int lastIndexOf(Object o) {...}

                                // Queue operations.
                                //部分省略

                                //always return true
                                public boolean offer(E e) {
                                return add(e);
                                }

                                // Deque operations
                                //省略

                                public boolean removeFirstOccurrence(Object o) {
                                //666
                                return remove(o);
                                }

                                public boolean removeLastOccurrence(Object o) {...}

                                public ListIterator<E> listIterator(int index) {
                                checkPositionIndex(index);
                                return new ListItr(index);
                                }

                                //非常聪明非常漂亮的指针操作
                                private class ListItr implements ListIterator<E> {
                                private Node<E> lastReturned;
                                private Node<E> next;
                                private int nextIndex;
                                private int expectedModCount = modCount;

                                ListItr(int index) {
                                // assert isPositionIndex(index);
                                next = (index == size) ? null : node(index);
                                nextIndex = index;
                                }

                                public boolean hasNext() {
                                return nextIndex < size;
                                }

                                public E next() {
                                checkForComodification();
                                if (!hasNext())
                                throw new NoSuchElementException();

                                lastReturned = next;
                                next = next.next;
                                nextIndex++;
                                return lastReturned.item;
                                }

                                public boolean hasPrevious() {
                                return nextIndex > 0;
                                }

                                public E previous() {
                                checkForComodification();
                                if (!hasPrevious())
                                throw new NoSuchElementException();

                                //注意这个next是内部类里的成员变量,last是外部类的成员变量
                                lastReturned = next = (next == null) ? last : next.prev;
                                nextIndex--;
                                return lastReturned.item;
                                }

                                public int nextIndex() {
                                return nextIndex;
                                }

                                public int previousIndex() {
                                return nextIndex - 1;
                                }

                                public void remove() {
                                checkForComodification();
                                if (lastReturned == null)
                                throw new IllegalStateException();

                                Node<E> lastNext = lastReturned.next;
                                unlink(lastReturned);
                                if (next == lastReturned)
                                next = lastNext;
                                else
                                nextIndex--;
                                lastReturned = null;
                                expectedModCount++;
                                }

                                public void set(E e) {
                                if (lastReturned == null)
                                throw new IllegalStateException();
                                checkForComodification();
                                lastReturned.item = e;
                                }

                                public void add(E e) {
                                checkForComodification();
                                lastReturned = null;
                                if (next == null)
                                linkLast(e);
                                else
                                linkBefore(e, next);
                                nextIndex++;
                                expectedModCount++;
                                }

                                public void forEachRemaining(Consumer<? super E> action) {
                                Objects.requireNonNull(action);
                                while (modCount == expectedModCount && nextIndex < size) {
                                action.accept(next.item);
                                lastReturned = next;
                                next = next.next;
                                nextIndex++;
                                }
                                checkForComodification();
                                }

                                final void checkForComodification() {
                                if (modCount != expectedModCount)
                                throw new ConcurrentModificationException();
                                }
                                }

                                //节点类,平平无奇链表捏
                                private static class Node<E> {
                                E item;
                                Node<E> next;
                                Node<E> prev;

                                Node(Node<E> prev, E element, Node<E> next) {
                                this.item = element;
                                this.next = next;
                                this.prev = prev;
                                }
                                }

                                public Iterator<E> descendingIterator() {
                                return new DescendingIterator();
                                }

                                // 2 降序迭代器
                                private class DescendingIterator implements Iterator<E> {
                                //借助升序迭代器实现
                                private final ListItr itr = new ListItr(size());

                                public boolean hasNext() {
                                return itr.hasPrevious();
                                }
                                public E next() {
                                return itr.previous();
                                }
                                public void remove() {
                                itr.remove();
                                }
                                }

                                @SuppressWarnings("unchecked")
                                private LinkedList<E> superClone() {
                                try {
                                return (LinkedList<E>) super.clone();
                                } catch (CloneNotSupportedException e) {
                                throw new InternalError(e);
                                }
                                }

                                public Object clone() {
                                LinkedList<E> clone = superClone();

                                // 初始化
                                clone.first = clone.last = null;
                                clone.size = 0;
                                clone.modCount = 0;

                                // Initialize clone with our elements
                                for (Node<E> x = first; x != null; x = x.next)
                                clone.add(x.item);

                                return clone;
                                }

                                public Object[] toArray() {...}

                                @SuppressWarnings("unchecked")
                                public <T> T[] toArray(T[] a) {...}

                                private static final long serialVersionUID = 876323262645176354L;

                                private void writeObject(java.io.ObjectOutputStream s)
                                throws java.io.IOException {...}

                                @SuppressWarnings("unchecked")
                                private void readObject(java.io.ObjectInputStream s)
                                throws java.io.IOException, ClassNotFoundException {...}

                                @Override
                                public Spliterator<E> spliterator() {
                                return new LLSpliterator<E>(this, -1, 0);
                                }

                                static final class LLSpliterator<E> implements Spliterator<E> {...}
                                //4
                                }
                                +

                                总之意思差不多就是,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. 关于分代GC

                                  In the clear() function:

                                  -
                                  // Clearing all of the links between nodes is "unnecessary", but:
                                  // - helps a generational GC if the discarded nodes inhabit
                                  // more than one generation
                                  // - is sure to free memory even if there is a reachable Iterator
                                  +

                                  其中:

                                    +
                                  1. hash()

                                    -

                                    还没看懂,插个眼

                                    -
                                  2. -
                                  3. 降序迭代器

                                    一切都反过来了,也没有升序迭代器恁多方法:

                                    -

                                    不支持foreach循环,只支持单向遍历,没有add set 只有remove。

                                    -
                                  4. -
                                  5. 关于unlinkLast/First的参数问题

                                    In Java LinkedList source code, why the unlinkFirst function should have a param pointing to the first node?

                                    -

                                    事实证明确实人家也觉得无参比较合理(

                                    +

                                    hash=原hashcode^(原hashcode逻辑右移16位)

                                    +

                                    这样的话,由于右移16位补零,此时高位的所有比特位都跟原来一样,低位的比特位变成了融合高低位特点的东西,这样就可以减少冲突,增加均匀性

                                  6. -
                                  7. sublist

                                    LinkedList用了从AbstractList继承来的sublist相关类和方法,没有特别的优化,其sublist不可序列化,且not cloneable。

                                    +
                                  8. 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取余的取余操作。这点实在是佩服啊,把复杂的取余操作在该场景下直接用一个位运算就搞定了。

                                  9. -
                                  10. transient的三个成员变量

                                    In Java LinkedList, why the “first”, “last” and “size” variable are transient?

                                    -
                                    -

                                    LinkedList provide its own method for serializing and de-serializing.

                                    -

                                    When serializing, it only writes the size, and the values of the list.

                                    -

                                    When deserializing, it reads the size, then build the list from scratch, each node for each value at a time.

                                    -

                                    If the author did not provide their own read and write methods, then they would need to make size, first, and last non-transient. They would also need to make the Node class serializable.

                                    -
                                    -

                                    As all the member variables of java LinkedList is transient, what will be the use of implementing Serializable?

                                    -
                                    -

                                    Otherwise serializing would be by default, which would be recursive, and for a large list would easily blow the stack.

                                    -
                                    -

                                    意思就是序列化时不记录这些信息,反序列化时会重新构建。还有说如果用默认的序列化方法是递归的可能爆栈?还有我觉得有一点可能是如果把所有node都序列化了,可能反序列化后,本来分配到那段内存空间要是被占用了,但指针值不变还是会有问题?等待之后解答。

                                    +
                                  11. 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。

                                  12. -
                                  -

                                  Vector

                                  -

                                  Unlike the new collection implementations, Vector is synchronized. If a thread-safe implementation is not needed, it is recommended to use ArrayList in place of Vector.

                                  -
                                  -

                                  代码:

                                  public class Vector<E>
                                  extends AbstractList<E>
                                  implements List<E>, RandomAccess, Cloneable, java.io.Serializable
                                  {

                                  protected Object[] elementData;

                                  protected int elementCount;

                                  protected int capacityIncrement;

                                  private static final long serialVersionUID = -2767605614048989439L;

                                  public Vector(int initialCapacity, int capacityIncrement) {
                                  super();
                                  if (initialCapacity < 0)
                                  throw new IllegalArgumentException("Illegal Capacity: "+
                                  initialCapacity);
                                  this.elementData = new Object[initialCapacity];
                                  this.capacityIncrement = capacityIncrement;
                                  }

                                  public Vector(int initialCapacity) {
                                  this(initialCapacity, 0);
                                  }

                                  public Vector() {
                                  //1
                                  this(10);
                                  }

                                  public Vector(Collection<? extends E> c) {
                                  Object[] a = c.toArray();
                                  elementCount = a.length;
                                  if (c.getClass() == ArrayList.class) {
                                  elementData = a;
                                  } else {
                                  elementData = Arrays.copyOf(a, elementCount, Object[].class);
                                  }
                                  }

                                  public synchronized void copyInto(Object[] anArray) {
                                  System.arraycopy(elementData, 0, anArray, 0, elementCount);
                                  }

                                  //2
                                  public synchronized void trimToSize() {
                                  modCount++;
                                  int oldCapacity = elementData.length;
                                  if (elementCount < oldCapacity) {
                                  elementData = Arrays.copyOf(elementData, elementCount);
                                  }
                                  }

                                  public synchronized void ensureCapacity(int minCapacity) {
                                  if (minCapacity > 0) {
                                  modCount++;
                                  ensureCapacityHelper(minCapacity);
                                  }
                                  }

                                  private void ensureCapacityHelper(int minCapacity) {
                                  // overflow-conscious code
                                  if (minCapacity - elementData.length > 0)
                                  grow(minCapacity);
                                  }

                                  private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

                                  private void grow(int minCapacity) {...}

                                  private static int hugeCapacity(int minCapacity) {...}

                                  /*
                                  Sets the size of this vector. If the new size is greater than the current size, new null items are added to the end of the vector. If the new size is less than the current size, all components at index newSize and greater are discarded.
                                  */
                                  public synchronized void setSize(int newSize) {
                                  modCount++;
                                  if (newSize > elementCount) {
                                  ensureCapacityHelper(newSize);
                                  } else {
                                  for (int i = newSize ; i < elementCount ; i++)
                                  //3 GC帮大忙,注意这里没有trim。
                                  elementData[i] = null;
                                  }
                                  }
                                  elementCount = newSize;
                                  }

                                  //capacity 、size 、isEmpty省略

                                  //4
                                  public Enumeration<E> elements() {
                                  return new Enumeration<E>() {
                                  int count = 0;
                                  public boolean hasMoreElements() {
                                  return count < elementCount;
                                  }
                                  public E nextElement() {
                                  synchronized (Vector.this) {
                                  if (count < elementCount) {
                                  return elementData(count++);
                                  }
                                  }
                                  throw new NoSuchElementException("Vector Enumeration");
                                  }
                                  };
                                  }

                                  //contains、indexof、lastIndexOf省略

                                  public synchronized E elementAt(int index) {
                                  if (index >= elementCount) {
                                  throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
                                  }

                                  return elementData(index);
                                  }

                                  public synchronized E firstElement() {
                                  if (elementCount == 0) {
                                  throw new NoSuchElementException();
                                  }
                                  return elementData(0);
                                  }

                                  public synchronized E lastElement() {
                                  if (elementCount == 0) {
                                  throw new NoSuchElementException();
                                  }
                                  return elementData(elementCount - 1);
                                  }

                                  public synchronized void setElementAt(E obj, int index) {...}

                                  public synchronized void removeElementAt(int index) {...}

                                  public synchronized void insertElementAt(E obj, int index) {...}

                                  public synchronized void addElement(E obj) {...}

                                  public synchronized boolean removeElement(Object obj) {...}

                                  //clear
                                  public synchronized void removeAllElements() {
                                  modCount++;
                                  // Let gc do its work
                                  for (int i = 0; i < elementCount; i++)
                                  elementData[i] = null;

                                  elementCount = 0;
                                  }

                                  public synchronized Object clone() {...}

                                  public synchronized Object[] toArray() {
                                  return Arrays.copyOf(elementData, elementCount);
                                  }

                                  @SuppressWarnings("unchecked")
                                  public synchronized <T> T[] toArray(T[] a) {...}

                                  // Positional Access Operations
                                  //add set get remove clear省略

                                  // Bulk Operations
                                  // xxxAll省略

                                  //用了AbstractList的equal、hashcode、tostring,省略

                                  public synchronized List<E> subList(int fromIndex, int toIndex) {
                                  return Collections.synchronizedList(super.subList(fromIndex, toIndex),
                                  this);
                                  }

                                  protected synchronized void removeRange(int fromIndex, int toIndex) {...}

                                  //跟AL不一样
                                  private void readObject(ObjectInputStream in)
                                  throws IOException, ClassNotFoundException {
                                  ObjectInputStream.GetField gfields = in.readFields();
                                  int count = gfields.get("elementCount", 0);
                                  Object[] data = (Object[])gfields.get("elementData", null);
                                  if (count < 0 || data == null || count > data.length) {
                                  throw new StreamCorruptedException("Inconsistent vector internals");
                                  }
                                  elementCount = count;
                                  elementData = data.clone();
                                  }

                                  private void writeObject(java.io.ObjectOutputStream s)
                                  throws java.io.IOException {
                                  final java.io.ObjectOutputStream.PutField fields = s.putFields();
                                  final Object[] data;
                                  synchronized (this) {
                                  fields.put("capacityIncrement", capacityIncrement);
                                  fields.put("elementCount", elementCount);
                                  data = elementData.clone();
                                  }
                                  fields.put("elementData", data);
                                  s.writeFields();
                                  }

                                  public synchronized ListIterator<E> listIterator(int index) {
                                  if (index < 0 || index > elementCount)
                                  throw new IndexOutOfBoundsException("Index: "+index);
                                  return new ListItr(index);
                                  }

                                  public synchronized ListIterator<E> listIterator() {
                                  return new ListItr(0);
                                  }

                                  public synchronized Iterator<E> iterator() {
                                  return new Itr();
                                  }

                                  private class Itr implements Iterator<E> {...}

                                  final class ListItr extends Itr implements ListIterator<E> {...}

                                  @Override
                                  public synchronized void forEach(Consumer<? super E> action) {...}

                                  @Override
                                  @SuppressWarnings("unchecked")
                                  public synchronized boolean removeIf(Predicate<? super E> filter) {...}

                                  @Override
                                  @SuppressWarnings("unchecked")
                                  public synchronized void replaceAll(UnaryOperator<E> operator) {...}

                                  @SuppressWarnings("unchecked")
                                  @Override
                                  public synchronized void sort(Comparator<? super E> c) {...}

                                  @Override
                                  public Spliterator<E> spliterator() {
                                  return new VectorSpliterator<>(this, null, 0, -1, 0);
                                  }

                                  static final class VectorSpliterator<E> implements Spliterator<E> {...}
                                  }
                                  - -

                                  其中:

                                    -
                                  1. 默认容量

                                    空构造器Vector()创建出来的默认容量为10,不同于ArrayList是个空集合。

                                    +
                                  2. entrySet

                                    不同于AbstractMap中entrySet的核心作用,HashMap的put、get、clear等等等核心函数都不依赖于entrySet了,毕竟结构改变得比较多了。因而这里的entrySet字段保留,只是为了呼应AbstractMap中keyset和valueset的实现,以及补充AbstractMap中未给出的EntrySet实现。

                                  3. -
                                  4. 扩容操作

                                    与ArrayList基本上是雷同的,就是都是synchronized。

                                    +
                                  5. 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;
                                  6. +
                                  7. 红黑树

                                    红黑树快速入门

                                    +

                                    这篇文章也写得很好:

                                    +

                                    算法:基于红黑树的 TreeMap

                                  8. -
                                  9. setsize不改变容量

                                    实现中只是把东西设置为空,并没有trim,因而容量不变

                                    +
                                  10. HashIterator

                                    注意点有二:

                                    +

                                    ①不继承Iterator接口

                                    +

                                    ②抽象,具体实现类为EntryIterator、KeyIterator和ValueIterator

                                    +

                                    ③map的接口定义是没有iterator的,因此map不能通过hashiterator迭代,只能通过其vie来实现【三个具体实现类】

                                  11. -
                                  12. Vector可生成枚举类
                                  -

                                  Set

                                  Set(I)

                                  代码:

                                  /* 1
                                  Note: 当set里包含可变的对象时,要多加小心。
                                  The behavior of a set is not specified if the value of a set object is changed in a manner that affects equals comparisons.
                                  A special case of this prohibition is that it is not permissible for a set to contain itself as an element.
                                  */
                                  public interface Set<E> extends Collection<E> {
                                  // Query Operations
                                  int size();

                                  boolean isEmpty();

                                  boolean contains(Object o);
                                  /*Returns an iterator over the elements in this set.
                                  The elements are returned in no particular order
                                  (unless this set is an instance of some class that provides a guarantee).*/
                                  Iterator<E> iterator();

                                  Object[] toArray();

                                  <T> T[] toArray(T[] a);

                                  // Modification Operations

                                  /*
                                  If this set already contains the element,
                                  the call leaves the set unchanged and returns false.
                                  */
                                  boolean add(E e);

                                  boolean remove(Object o);

                                  // Bulk Operations

                                  /*
                                  Returns true if this set contains all of the elements of the specified collection.
                                  If the specified collection is also a set,
                                  this method returns true if it is a subset of this set.[这个subset的定义有点意思]
                                  */
                                  boolean containsAll(Collection<?> c);

                                  //2
                                  //取并集
                                  boolean addAll(Collection<? extends E> c);

                                  //取交集
                                  boolean retainAll(Collection<?> c);

                                  //集合差
                                  boolean removeAll(Collection<?> c);

                                  void clear();

                                  // Comparison and hashing

                                  boolean equals(Object o);

                                  int hashCode();

                                  @Override
                                  default Spliterator<E> spliterator() {
                                  return Spliterators.spliterator(this, Spliterator.DISTINCT);
                                  }
                                  }
                                  +

                                  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.

                                  + -

                                  其中:

                                    -
                                  1. set中包含可变对象

                                    需要规避可修改对象,使其与集合中另一个元素重复的问题

                                    -

                                    详见此:

                                    -

                                    Java HashSet contains duplicates if contained element is modified

                                    +

                                    这样可以保持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”怎么定义:

                                    -

                                    The correct solution is to stick to the contract of Set and not modify objects after adding them to the collection.

                                    -

                                    You can avoid this problem by either:

                                    -
                                      -
                                    • using an immutable type for your set elements,
                                    • -
                                    • making a copy of the objects as you put them into the set and / or pull them out of the set,
                                    • -
                                    • writing your code so that it “knows“ not to change the objects for the duration …
                                    • -
                                    -

                                    From the perspective of correctness and robustness, the first option is clearly best.

                                    +

                                    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

                                    -
                                  2. -
                                  3. 集合操作

                                    set抽象自数学的集合,因此有很多对应的集合操作:

                                    -
                                    -

                                    addAll ∪

                                    -

                                    retainAll ∩

                                    -

                                    removeAll -

                                    +

                                    也因此,对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也可以它却重写了。原来是因为这个性能问题啊

                                    -

                                    AbstratcSet(A)

                                    -

                                    Note that this class does not override any of the implementations from the AbstractCollection class. It merely adds implementations for equals and hashCode.

                                    +

                                    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

                                    -

                                    代码:

                                    public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E> {

                                    protected AbstractSet() {
                                    }

                                    // Comparison and hashing

                                    public boolean equals(Object o) {
                                    if (o == this)
                                    return true;

                                    if (!(o instanceof Set))
                                    return false;
                                    Collection<?> c = (Collection<?>) o;
                                    if (c.size() != size())
                                    return false;
                                    try {
                                    return containsAll(c);
                                    } catch (ClassCastException unused) {
                                    return false;
                                    } catch (NullPointerException unused) {
                                    return false;
                                    }
                                    }

                                    public int hashCode() {
                                    int h = 0;
                                    Iterator<E> i = iterator();
                                    while (i.hasNext()) {
                                    E obj = i.next();
                                    if (obj != null)
                                    h += obj.hashCode();
                                    }
                                    return h;
                                    }

                                    //不大明白为啥要修改实现,用AbstractCollection的不好吗
                                    public boolean removeAll(Collection<?> c) {
                                    Objects.requireNonNull(c);
                                    boolean modified = false;

                                    if (size() > c.size()) {
                                    for (Iterator<?> i = c.iterator(); i.hasNext(); )
                                    modified |= remove(i.next());
                                    } else {
                                    for (Iterator<?> i = iterator(); i.hasNext(); ) {
                                    if (c.contains(i.next())) {
                                    i.remove();
                                    modified = true;
                                    }
                                    }
                                    }
                                    return modified;
                                    }
                                    }
                                    +

                                    最大的特点就是可以人为定义有序并且有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();
                                    }
                                    -

                                    HashSet

                                    -

                                    In particular, it does not guarantee that the order will remain constant over time.

                                    -

                                    Iterating over this set requires time proportional to the sum of the HashSet instance’s size (the number of elements) plus the “capacity” of the backing HashMap instance (the number of buckets).

                                    -

                                    Thus, it’s very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important.

                                    -

                                    This implementation is not synchronized. Set s = Collections.synchronizedSet(new HashSet(...));

                                    +
                                    +

                                    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的快照

                                    -

                                    代码:

                                    public class HashSet<E>
                                    extends AbstractSet<E>
                                    implements Set<E>, Cloneable, java.io.Serializable
                                    {
                                    static final long serialVersionUID = -5024744406713321676L;

                                    //通过hashmap实现
                                    //map不可序列化
                                    private transient HashMap<E,Object> map;

                                    // Dummy value to associate with an Object in the backing Map
                                    // 1
                                    private static final Object PRESENT = new Object();

                                    //Constructs a new, empty set;
                                    //the backing HashMap instance has default initial capacity (16)
                                    //and load factor (0.75).
                                    public HashSet() {
                                    map = new HashMap<>();
                                    }

                                    //The HashMap is created with default load factor (0.75)
                                    //and an initial capacity sufficient to contain the elements in c
                                    public HashSet(Collection<? extends E> c) {
                                    //看来这个load factor=size/0.75
                                    map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
                                    addAll(c);
                                    }

                                    public HashSet(int initialCapacity, float loadFactor) {
                                    map = new HashMap<>(initialCapacity, loadFactor);
                                    }

                                    public HashSet(int initialCapacity) {
                                    map = new HashMap<>(initialCapacity);
                                    }

                                    //This package private constructor is only used by LinkedHashSet.
                                    //@param: dummy – ignored (distinguishes this constructor from other constructor.)
                                    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
                                    //此处为LinkeHashMap
                                    map = new LinkedHashMap<>(initialCapacity, loadFactor);
                                    }

                                    public Iterator<E> iterator() {return map.keySet().iterator();}

                                    public int size() {return map.size();}

                                    public boolean isEmpty() {return map.isEmpty();}

                                    public boolean contains(Object o) {return map.containsKey(o);}

                                    //@return true if this set did not already contain the specified element
                                    //map.put返回已有的oldValue,返回空表示没有oldValue,插入成功;否则失败
                                    public boolean add(E e) {
                                    return map.put(e, PRESENT)==null;
                                    }

                                    public boolean remove(Object o) {
                                    return map.remove(o)==PRESENT;
                                    }

                                    public void clear() {
                                    map.clear();
                                    }

                                    @SuppressWarnings("unchecked")
                                    public Object clone() {
                                    try {
                                    HashSet<E> newSet = (HashSet<E>) super.clone();
                                    newSet.map = (HashMap<E, Object>) map.clone();
                                    return newSet;
                                    } catch (CloneNotSupportedException e) {
                                    throw new InternalError(e);
                                    }
                                    }

                                    private void writeObject(java.io.ObjectOutputStream s)
                                    throws java.io.IOException {...}

                                    private void readObject(java.io.ObjectInputStream s)
                                    throws java.io.IOException, ClassNotFoundException {...}

                                    public Spliterator<E> spliterator() {
                                    return new HashMap.KeySpliterator<E,Object>(map, 0, -1, 0, 0);
                                    }
                                    }
                                    +

                                    跟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);
                                    }
                                    -

                                    其中:

                                      -
                                    1. PRESENT

                                      正如它的解释:

                                      -
                                      -

                                      Dummy value to associate with an Object in the backing Map

                                      +

                                      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的性质】

                                      -

                                      set 以map作为内部支持,其实主要用的是map对于key的高效去重。也就是说,set其实只需要用map的key这一半就好了。所以我们另一半value就都统一用一个new Object【也就是PRESENT】来统一就行。

                                      -

                                      不得不说这点很聪明,值得学习。

                                      -
                                      private static final Object PRESENT = new Object();
                                      public boolean add(E e) {
                                      return map.put(e, PRESENT)==null;
                                      }
                                      public boolean remove(Object o) {
                                      return map.remove(o)==PRESENT;
                                      }
                                    2. -
                                    -

                                    SortedSet(I)

                                    -

                                    A Set that further provides a total ordering on its elements.

                                    -

                                    The set’s iterator will traverse the set in ascending element order.

                                    -

                                    All elements inserted into a sorted set must implement the Comparable interface (or be accepted by the specified comparator).否则导致ClassCastException

                                    -

                                    Note that the ordering maintained by a sorted set (whether or not an explicit comparator is provided) must be consistent with equals if the sorted set is to correctly implement the Set interface.

                                    -

                                    【consistent with equals:if and only if c.compare(e1, e2)==0 has the same boolean value as e1.equals(e2) for every e1 and e2 in S.】

                                    -

                                    //2

                                    -

                                    Note: several methods return subsets with restricted ranges. 区间是前闭后开的。

                                    -

                                    If you need a closed range , and the element type allows for calculation of the successor of a given value, merely request the subrange from lowEndpoint to successor(highEndpoint).

                                    -

                                    For example, suppose that s is a sorted set of strings. [low,hight]
                                    SortedSet<String> sub = s.subSet(low, high+"\0");
                                    A similar technique can be used to generate an open range (low,hight)
                                    SortedSet<String> sub = s.subSet(low+"\0", high);

                                    -

                                    66666

                                    +

                                    具体代码就不看了

                                    +

                                    对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.

                                      -

                                      代码

                                      public interface SortedSet<E> extends Set<E> {
                                      //null if this set uses the natural ordering
                                      Comparator<? super E> comparator();
                                      //1
                                      SortedSet<E> subSet(E fromElement, E toElement);

                                      SortedSet<E> headSet(E toElement);

                                      SortedSet<E> tailSet(E fromElement);

                                      E first();

                                      E last();

                                      @Override
                                      default Spliterator<E> spliterator() {
                                      return new Spliterators.IteratorSpliterator<E>(
                                      this, Spliterator.DISTINCT | Spliterator.SORTED | Spliterator.ORDERED) {
                                      @Override
                                      public Comparator<? super E> getComparator() {
                                      return SortedSet.this.comparator();
                                      }
                                      };
                                      }
                                      }
                                      - -

                                      其中:

                                        -
                                      1. subset

                                        只有sorted set才有subset,想想也确实

                                        -
                                        /*
                                        Throws:
                                        ClassCastException – if fromElement and toElement cannot be compared to one another using this set's comparator
                                        NullPointerException – if fromElement or toElement is null and this set does not permit null elements
                                        IllegalArgumentException – if fromElement is greater than toElement; or fromElement or toElement lies outside the bounds of the restricted range of the set
                                        */
                                        //The returned set will throw an IllegalArgumentException
                                        //on an attempt to insert an element outside its range.
                                        SortedSet<E> subSet(E fromElement, E toElement);
                                        - -

                                        以及注意此处是Element,不是Index

                                        +

                                        都使用了modcount进行并发检查,都具有fail-fast的特点(关于此的详细解说,可见AbstractList第四点和List第二点),因而只允许在迭代中使用迭代器的remove方法进行结构性改变。【注意:对于LinkedHashMap中access order排序,get方法也是structural modification,因而也只能通过迭代器的next方法获取元素】

                                      2. -
                                      3. subSet(low, high+”\0”);

                                        为什么加个”\0”就可以,具体可以看看这个:

                                        -

                                        Adding “\0” to a subset range end

                                        -

                                        原因就是sub的这个range取的是在此区间的元素,low和high这两个param不一定要包含在这个set里面。因此,按照set的排序,high+”\0”比high大,因而high就落入此区间,也就可以被包含在range中了。

                                        +
                                      4. not synchronized

                                        上面介绍到的几个类,除了Vector外,都是线程不同步的。可以用此方式让其线程同步。

                                        +
                                        Map m = Collections.synchronizedMap(new LinkedHashMap(...));
                                      5. +
                                      6. 是否允许null

                                        除了TreeSet、TreeMap、ArrayDeque之外,都是允许空(key/value)的

                                        +
                                      7. +
                                      8. 是否有序

                                        List都是插入序,HashSet无需,HashMap也无序(但其实算是有内部桶序的),LinkedHashMap有插入序和LRU序(依靠内部增加简单队列的消耗),TreeSet有序,TreeMap有序【这俩靠红黑树的遍历顺序(二叉搜索树嘛)】。

                                        +
                                      9. +
                                      10. 实现的约定接口

                                        都Cloneable,Serializable

                                        +

                                        ArrayList/Vector:RandomAccess

                                      -
                                      -

                                      比起sorted set,navigable set最特殊的点在于它提供了对某一元素附近元素的导航。

                                      -

                                      Method usage

                                      -

                                      lower less than最大的,比所给ele小的元素

                                      -

                                      floor less than or equal最大的,比所给ele小或者等于的元素

                                      -

                                      ceiling greater than or equal最小的,比所给ele大或者等于的元素

                                      -

                                      higher greater than最小的,比所给ele大的元素

                                      -

                                      The descendingSet method returns a view of the set with the senses of all relational and directional methods inverted.

                                      -

                                      This interface additionally defines methods pollFirst and pollLast that return and remove the lowest and highest element, if one exists, else returning null. 有点堆的感觉

                                      -

                                      Methods subSet, headSet, and tailSet differ from the like-named SortedSet methods in accepting additional arguments describing whether lower and upper bounds are inclusive versus exclusive.

                                      -
                                      -

                                      代码:

                                      public interface NavigableSet<E> extends SortedSet<E> {
                                      //Returns the greatest element in this set strictly less than the given element
                                      E lower(E e);

                                      E floor(E e);

                                      E ceiling(E e);

                                      E higher(E e);

                                      //Removes the first (lowest) element, or returns null if this set is empty.
                                      E pollFirst();

                                      E pollLast();

                                      //in ascending order
                                      Iterator<E> iterator();

                                      /*
                                      The returned set has an ordering equivalent to
                                      Collections.reverseOrder(comparator()).
                                      The expression s.descendingSet().descendingSet()
                                      returns a view of s essentially equivalent(基本等价) to s
                                      */
                                      NavigableSet<E> descendingSet();

                                      //Equivalent in effect to descendingSet().iterator()
                                      Iterator<E> descendingIterator();

                                      NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
                                      E toElement, boolean toInclusive);

                                      NavigableSet<E> headSet(E toElement, boolean inclusive);

                                      NavigableSet<E> tailSet(E fromElement, boolean inclusive);

                                      SortedSet<E> subSet(E fromElement, E toElement);

                                      SortedSet<E> headSet(E toElement);

                                      SortedSet<E> tailSet(E fromElement);
                                      }
                                      - -

                                      TreeSet

                                      -

                                      This implementation provides guaranteed log(n) time cost for the basic operations (add, remove and contains).

                                      -

                                      Note that this implementation is not synchronized.

                                      -

                                      SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...));

                                      -
                                      -

                                      代码:

                                      public class TreeSet<E> extends AbstractSet<E>
                                      implements NavigableSet<E>, Cloneable, java.io.Serializable
                                      {

                                      //依然用了个map
                                      private transient NavigableMap<E,Object> m;

                                      // Dummy value to associate with an Object in the backing Map
                                      private static final Object PRESENT = new Object();

                                      TreeSet(NavigableMap<E,Object> m) {
                                      this.m = m;
                                      }

                                      public TreeSet() {
                                      this(new TreeMap<E,Object>());
                                      }

                                      public TreeSet(Comparator<? super E> comparator) {
                                      this(new TreeMap<>(comparator));
                                      }

                                      public TreeSet(Collection<? extends E> c) {
                                      this();
                                      addAll(c);
                                      }

                                      public TreeSet(SortedSet<E> s) {
                                      this(s.comparator());
                                      addAll(s);
                                      }

                                      public Iterator<E> iterator() {
                                      return m.navigableKeySet().iterator();
                                      }

                                      public Iterator<E> descendingIterator() {
                                      return m.descendingKeySet().iterator();
                                      }

                                      //确实直接让map倒序就可以了
                                      public NavigableSet<E> descendingSet() {
                                      return new TreeSet<>(m.descendingMap());
                                      }

                                      public int size() {
                                      return m.size();
                                      }

                                      public boolean isEmpty() {
                                      return m.isEmpty();
                                      }

                                      public boolean contains(Object o) {
                                      return m.containsKey(o);
                                      }

                                      public boolean add(E e) {
                                      return m.put(e, PRESENT)==null;
                                      }

                                      public boolean remove(Object o) {
                                      return m.remove(o)==PRESENT;
                                      }

                                      public void clear() {
                                      m.clear();
                                      }

                                      public boolean addAll(Collection<? extends E> c) {
                                      // Use linear-time version if applicable
                                      if (m.size()==0 && c.size() > 0 &&
                                      c instanceof SortedSet &&
                                      m instanceof TreeMap) {
                                      SortedSet<? extends E> set = (SortedSet<? extends E>) c;
                                      TreeMap<E,Object> map = (TreeMap<E, Object>) m;
                                      Comparator<?> cc = set.comparator();
                                      Comparator<? super E> mc = map.comparator();
                                      if (cc==mc || (cc != null && cc.equals(mc))) {
                                      map.addAllForTreeSet(set, PRESENT);
                                      return true;
                                      }
                                      }
                                      return super.addAll(c);
                                      }

                                      public NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
                                      E toElement, boolean toInclusive) {
                                      return new TreeSet<>(m.subMap(fromElement, fromInclusive,
                                      toElement, toInclusive));
                                      }

                                      public NavigableSet<E> headSet(E toElement, boolean inclusive) {
                                      return new TreeSet<>(m.headMap(toElement, inclusive));
                                      }

                                      public NavigableSet<E> tailSet(E fromElement, boolean inclusive) {
                                      return new TreeSet<>(m.tailMap(fromElement, inclusive));
                                      }

                                      public SortedSet<E> subSet(E fromElement, E toElement) {
                                      return subSet(fromElement, true, toElement, false);
                                      }

                                      public SortedSet<E> headSet(E toElement) {
                                      return headSet(toElement, false);
                                      }

                                      //注意此处为true。严格遵循左闭右开
                                      public SortedSet<E> tailSet(E fromElement) {
                                      return tailSet(fromElement, true);
                                      }

                                      public Comparator<? super E> comparator() {
                                      return m.comparator();
                                      }

                                      public E first() {
                                      return m.firstKey();
                                      }

                                      public E last() {
                                      return m.lastKey();
                                      }

                                      // NavigableSet API methods

                                      public E lower(E e) {
                                      return m.lowerKey(e);
                                      }

                                      public E floor(E e) {
                                      return m.floorKey(e);
                                      }

                                      public E ceiling(E e) {
                                      return m.ceilingKey(e);
                                      }

                                      public E higher(E e) {
                                      return m.higherKey(e);
                                      }

                                      public E pollFirst() {
                                      Map.Entry<E,?> e = m.pollFirstEntry();
                                      return (e == null) ? null : e.getKey();
                                      }

                                      public E pollLast() {
                                      Map.Entry<E,?> e = m.pollLastEntry();
                                      return (e == null) ? null : e.getKey();
                                      }

                                      @SuppressWarnings("unchecked")
                                      public Object clone() {
                                      TreeSet<E> clone;
                                      try {
                                      clone = (TreeSet<E>) super.clone();
                                      } catch (CloneNotSupportedException e) {
                                      throw new InternalError(e);
                                      }

                                      clone.m = new TreeMap<>(m);
                                      return clone;
                                      }

                                      private void writeObject(java.io.ObjectOutputStream s)
                                      throws java.io.IOException {...}

                                      private void readObject(java.io.ObjectInputStream s)
                                      throws java.io.IOException, ClassNotFoundException {...}

                                      public Spliterator<E> spliterator() {
                                      return TreeMap.keySpliteratorFor(m);
                                      }

                                      private static final long serialVersionUID = -2479143000061671589L;
                                      }
                                      - ]]> Java diff --git a/tag/index.html b/tag/index.html new file mode 100644 index 00000000..aab2b00c --- /dev/null +++ b/tag/index.html @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + +Tag | 修年 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                                      +
                                      + + + + + +
                                      + +
                                      + + +
                                      +
                                      +

                                      - Tag Cloud -

                                      + +
                                      + + os竞赛(2) + + books(4) + + labs(4) + + Java(3) + + intern(2) + +
                                      +
                                      +
                                      + + +
                                      +
                                      + +
                                      + +
                                      + + + \ No newline at end of file diff --git a/tags/Java/index.html b/tags/Java/index.html index 16e7665a..0d4a276a 100644 --- a/tags/Java/index.html +++ b/tags/Java/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/tags/books/index.html b/tags/books/index.html index 653f90f1..7b96e9f9 100644 --- a/tags/books/index.html +++ b/tags/books/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/tags/intern/index.html b/tags/intern/index.html index 3dd04ee0..8b6467f0 100644 --- a/tags/intern/index.html +++ b/tags/intern/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -142,6 +146,20 @@

                                      2023

                                      + + + + + + + + + + + diff --git a/tags/labs/index.html b/tags/labs/index.html index 8ec5988f..204b1093 100644 --- a/tags/labs/index.html +++ b/tags/labs/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + diff --git a/tags/mylife/index.html b/tags/mylife/index.html index 896fd410..6810fb9b 100644 --- a/tags/mylife/index.html +++ b/tags/mylife/index.html @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + 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 3573807b..e767e2c2 100644 --- "a/tags/os\347\253\236\350\265\233/index.html" +++ "b/tags/os\347\253\236\350\265\233/index.html" @@ -76,6 +76,8 @@ About + Tags + @@ -97,6 +99,8 @@ About + Tags + @@ -136,6 +140,20 @@

                                      2023

                                      + + + + + + + + + + +