0%

Relational Databases 关系数据库

Database Design

Data Storage and Query

Transaction Management

Relational Model

Structure 结构

关系模型 中:每一个表是一个关系,每一行是一个元组,每一列是一个属性。

  • A relational database consists of a collection of tables.
  • A row in a table represents a relationship among a set of values.
  • The order in which tuples appear in a relation is irrelevant, since a relation is a set of tuples .

Domain 域

域就是属性的值域(取值集合)。原子性意为“不可再分”。

  • For each attribute of a relation, there is a set of permitted values , called the domain of that attribute.

  • For all relations r , the domains of all attributes of r be atomic.

Null Value 空值

  • 空值是一种特殊值,表示:取值未知或不存在。
  • 任何域都含有空值。

Schema 模式

Database schema : the logical design of the database.

Database instance : a snapshot of the data in the database at a given instant in time.

例如:instructor = (ID, name, dept_name, salary)

Keys 键

We must have a way to specify how tuples within a given relation are distinguished.

  • superkey : A superkey is a set of one or more attributes that, taken collectively, allow us to identify uniquely a tuple in the relation.
  • candidate key : The superkeys, for which no proper subset is a superkey, are called candidate keys.
  • primary key : A primary key is a candidate key that is chosen by the database designer as the principal means of identifying tuples within a relation.
  • foreign key : A relation, may include among its attributes the primary key of another relation. This attribute is called a foreign key.

注意:

  1. A superkey may contain extraneous attributes.

  2. It is possible that several distinct sets of attributes could
    serve as a candidate key.

Schema Diagrams 模式图

Schema diagram = database schema + primary key and foreign key dependencies

Schema Diagrams

Query Languages

A query language is a language in which a user requests information from the database.

Query Languages 通常有两类:

  • procedural:指定查询什么数据以及如何获取数据。
  • declarative:只需指定查询什么数据,不必指定如何查询数据。

Relational Algebra

SQL

What is SQL

数据库系统提供:

  • data-definition language (DDL):定义数据库模式。
  • data-manipulation language (DML):表达数据库查询和更新。

但实际上,DDLDML 并不是两种分离的语言。相反地,它们简单地构成单一的数据库语言,比如:SQL。

查询 (query) 是对所求信息进行检索的语句。DML中涉及信息检索的部分称作 query language 。但实践中常将 query language 和 DML 视为同义词。

Domain Types

  • char(n):固定长度字符串(长度为 n )
  • varchar(n):变长度字符串(最大长度为 n )
  • int:整数
  • smallint:小整数
  • numeric(p, d):固定点数(有效数字 p 位,小数点后 d 位)
  • realdouble percision:实数、双精度浮点数
  • float(n):浮点数(有效数字 n 位)

Common Operation

创建 table

1
2
3
4
5
6
7
8
CREATE TABLE Users(
user_id char(6) NOT Null,
username varchar(30),
realname varchar(30),
age int,
password varchar(30),
permisson int
);

创建 procedure

1

Relational Database Design

函数依赖

函数依赖本质上就是:两个属性之间存在单射

设R是一个关系模式,X 和 Y 是 R 的属性集的子集。如果对于 R 的任意一个关系r,对于r中的任意两个元组 t1 和 t2 ,只要 t1 和 t2 在 X 上的值相等,那么它们在 Y 上的值也相等,那么我们就说 Y 在 X 上函数依赖,记作 X->Y

K is a superkey for R ↔ K -> R

K is a candidate key for R ↔ K->R and no αK,   R

第一范式

R 满足第一范式 ↔ R的所有属性的域都是原子的。

整体架构

架构图

我设计的RV64-CPU总共有14个模块:

  • PC:更新下一条指令地址。
  • PCincrement:将PC加4,作为PCNext的可能值。
  • PCNext:根据Jump和CndCode信号,判断PC是跳转还是累加,计算出PCNext值。
  • InstMem:指令内存。根据PC值取出指令并解析。
  • RegFile:寄存器文件。执行读取和写回操作。
  • ImmGen:立即数生成器。将立即数补位成64位。
  • Control:控制模块。判断指令类型,并输出控制信号。
  • ALU_A:选择ALU端口A的数据来源。
  • ALU_B:选择ALU端口B的数据来源。
  • ALU_control:选择ALU功能。
  • ALU:执行算术逻辑运算。
  • DataMem:数据内存。执行写入和读取内存操作。
  • Conditional:计算Conditional Code。
  • RegBack:选择写回寄存器的数据来源。

其中控制信号有以下:

  • Branch:是否是Branch类指令
  • Jump:是否是jal指令
  • JumpReg:是否是jalr指令
  • MemRead:是否读取内存
  • MemWrite:是否写入内存
  • ALUop:指令类型
  • ALUSrc:ALU_B数据来源
  • RegWrite:是否写回寄存器
  • Halt:停止执行

我没有设置MemtoReg信号。这是因为:目前,MemtoReg信号和MemRead信号一致,读取内存一定是存进寄存器里。

RISC-V指令集

我设计的CPU支持所有RV32I Base Integer Instruction(除ecallebreak外)。

指令集

代码实现

PC

在时钟上升沿更新PC值。

1
2
3
4
5
6
7
8
9
module PC(
input logic clk,
input logic [63:0] PCnext,
output logic [63:0] PCaddress
);
always_ff @(posedge clk) begin
PCaddress <= PCnext;
end
endmodule

PCincrement

计算PC累加值,作为PCNext的可能值。

1
2
3
4
5
6
7
8
9
module PCincrement(
input logic [63:0] PCaddress,
output logic [63:0] PCincrement
);
always_comb begin
PCincrement = PCaddress + 4;
end
endmodule

PCNext

根据Conditional CodeJumpJumpReg信号,计算PCNext值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module PCNext(
input logic [63:0] PCaddress,
input logic [63:0] PCincrement,
input logic [63:0] Imm,
input logic [63:0] Rs1, // jalr会用到rs1
input logic Jump, // jal
input logic JumpReg, // jalr
input logic CndCode, // branch
input logic Halt,
output logic [63:0] PCnext
);
always_comb begin
if(CndCode) PCnext = PCaddress + Imm;
else if(Jump) PCnext = PCaddress + Imm;
else if(JumpReg) PCnext = Rs1 + Imm;
else if(Halt) PCnext = PCaddress;
else PCnext = PCincrement;
end
endmodule

Control

根据OpCode判断指令类型,并产生控制信号,指导:ALU运算、寄存器读写、内存读写、PC更新。

ALUop 指令类型
000 R-type
001 I1-type
010 I2-type(Load类指令)
011 S-tpye
100 B-type
101 J-type
110 U-type
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
module Control(
input logic [6:0] OpCode,
output logic Branch,
output logic Jump,
output logic JumpReg,
output logic MemRead,
output logic MemWrite,
output logic [2:0] ALUop, // 指令类型
output logic ALUSrc, // 0:Rs2 1:imm
output logic RegWrite,
output logic Halt // 1:停机 0:正常运行
);
/*
*ALUop:
*000:R-type 001:I1-type 010:I2-type 011:S-type
*100:B-type 101:J-type 110:U-type
*/

always_comb begin
case(OpCode)
7'b0110011: begin // R-type
Branch = 0; Jump = 0; JumpReg = 0;
MemRead = 0; MemWrite = 0;
ALUop = 3'b000; ALUSrc = 0;
RegWrite = 1;
Halt = 0;
end
7'b0010011: begin // I1-type
Branch = 0; Jump = 0; JumpReg = 0;
MemRead = 0; MemWrite = 0;
ALUop = 3'b001; ALUSrc = 1;
RegWrite = 1;
Halt = 0;
end
7'b0000011: begin // I2-type
Branch = 0; Jump = 0; JumpReg = 0;
MemRead = 1; MemWrite = 0;
ALUop = 3'b010; ALUSrc = 1;
RegWrite = 1;
Halt = 0;
end
7'b0100011: begin // S-type
Branch = 0; Jump = 0; JumpReg = 0;
MemRead = 0; MemWrite = 1;
ALUop = 3'b011; ALUSrc = 1;
RegWrite = 0;
Halt = 0;
end
7'b1100011: begin // B-type
Branch = 1; Jump = 0; JumpReg = 0;
MemRead = 0; MemWrite = 0;
ALUop = 3'b100; ALUSrc = 0;
RegWrite = 0;
Halt = 0;
end
7'b1101111: begin // J-type (jal)
Branch = 0; Jump = 1; JumpReg = 0;
MemRead = 0; MemWrite = 0;
ALUop = 3'b101; ALUSrc = 0;
RegWrite = 0;
Halt = 0;
end
7'b1100111: begin // jarl
Branch = 0; Jump = 0; JumpReg = 1;
MemRead = 0; MemWrite = 0;
ALUop = 3'b101; ALUSrc = 1;
RegWrite = 1;
Halt = 0;
end
7'b0110111: begin // U-type(lui)
Branch = 0; Jump = 0; JumpReg = 0;
MemRead = 0; MemWrite = 0;
ALUop = 3'b110; ALUSrc = 1;
RegWrite = 1;
Halt = 0;
end
7'b0010111: begin // U-type(auipc)
Branch = 0; Jump = 0; JumpReg = 0;
MemRead = 0; MemWrite = 0;
ALUop = 3'b110; ALUSrc = 1;
RegWrite = 1;
Halt = 0;
end
default: begin
Branch = 0; Jump = 0; JumpReg = 0;
MemRead = 0; MemWrite = 0;
ALUop = 3'b000; ALUSrc = 0;
RegWrite = 0;
Halt = 1;
end
endcase
end
endmodule

InstMem

根据PCaddress读取指令,并拆解为OpCodeRs1Rs2RdInst是完整指令,用于生成立即数。

为了后续便于验证正确性,这里预先写入两条指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
module InstMem(
input logic [63:0] PCaddress,
output logic [6:0] OpCode,
output logic [4:0] Rs1,
output logic [4:0] Rs2,
output logic [4:0] Rd,
output logic [31:0] Inst
);
logic [7:0] mem[0:1023];
logic [31:0] instruction;

initial begin
mem[0] = 8'b10110111; // b7 ( lui x5, 0x12345)
mem[1] = 8'b00000010; // 02
mem[2] = 8'b01101000; // 68
mem[3] = 8'b00010010; // 12
mem[4] = 8'b10110011; // b3 ( add x7, x5, x6)
mem[5] = 8'b00000011; // 03
mem[6] = 8'b01010011; // 53
mem[7] = 8'b00000000; // 00
end

always_comb begin
instruction = {mem[PCaddress+3], mem[PCaddress+2], mem[PCaddress+1], mem[PCaddress]};
OpCode = instruction[6:0];
Rs1 = instruction[19:15];
Rs2 = instruction[24:20];
Rd = instruction[11:7];
Inst = instruction;
end
endmodule

RegFile

对寄存器文件进行读写操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module RegFile(
input logic clk,
input logic [4:0] ReadReg1,
input logic [4:0] ReadReg2,
input logic [4:0] WriteReg,
input logic RegWrite, // 是否写入寄存器
input logic [63:0] WriteData,
output logic [63:0] ReadData1,
output logic [63:0] ReadData2
);
logic [63:0] regs[0:31]; // 32个64-bit寄存器

always_ff @(posedge clk) begin // write是时序的
if(RegWrite) begin
regs[WriteReg] <= WriteData;
end
end

always_comb begin // read是组合的
ReadData1 = regs[ReadReg1];
ReadData2 = regs[ReadReg2];
end
endmodule

ImmGen

根据Inst判断指令类型,根据不同指令,采取不同方式补位立即数。

funct7_30是 funct7 的1个 bit ,指令中的第30个 bit 。funct7 中其余部分都是0,真正有区分作用的仅funct7_30一位。(区分sraisrli时)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
module ImmGen(
input logic [31:0] Inst,
output logic [63:0] Imm
);
logic [6:0] opcode;
logic [2:0] funct3;
logic funct7_30;

assign opcode = Inst[6:0];
assign funct3 = Inst[14:12];
assign funct7_30 = Inst[30];

always_comb begin
case(opcode)
7'b0010011: begin // I-type
if(funct3 == 3'b101 && funct7_30 == 1) begin
Imm ={{(64 - 5){Inst[24]}},Inst[24:20]};
end else begin
Imm = {{(64 - 12){Inst[31]}}, Inst[31:20]};
end
end
7'b0000011: Imm = {{(64 - 12){Inst[31]}}, Inst[31:20]}; // I-type(load)
7'b0100011: Imm = {{(64 - 12){Inst[31]}}, Inst[31:25], Inst[11:7]}; // S-type
7'b1100011: Imm = {{(64 - 13){Inst[31]}}, Inst[31], Inst[7], Inst[30:25], Inst[11:8], 1'b0}; // B-type
7'b1101111: Imm = {{(64 - 21){Inst[31]}}, Inst[31], Inst[19:12], Inst[20], Inst[30:21], 1'b0}; // J-type(jal)
7'b1100111: Imm = {{(64 - 12){Inst[31]}}, Inst[31:20]}; // jalr
7'b0110111: Imm = {{(64 - 32){Inst[31]}}, Inst[31:12], 12'b0}; // U-type(lui)
7'b0010111: Imm = {{(64 - 32){Inst[31]}}, Inst[31:12], 12'b0}; // U-type(auipc)
default: Imm = 64'b0;
endcase
end
endmodule

ALU_control

根据指令类型,选择ALU具体功能ALUfunc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
module ALU_control(
input logic [2:0] ALUop,
input logic [2:0] funct3,
input logic funct7_30,
output logic [2:0] ALUfunc
);
always_comb begin
case(ALUop)
3'b000: begin
case(funct3)
3'b000: begin
case(funct7_30)
1'b0: ALUfunc = 3'b000; // add
1'b1: ALUfunc = 3'b001; // sub
endcase
end
3'b001: ALUfunc = 3'b101; // sll(<<)
3'b010: ALUfunc = 3'b001; // slt(做减法运算)
3'b011: ALUfunc = 3'b001; // sltu(做减法运算)
3'b100: ALUfunc = 3'b100; // xor
3'b101: begin
case(funct7_30)
1'b0: ALUfunc = 3'b111; // srl(>>>)
1'b1: ALUfunc = 3'b110; // sra(>>)
endcase
end
3'b110: ALUfunc = 3'b011; // or
3'b111: ALUfunc = 3'b010; // and
endcase
end
3'b001: begin
case(funct3)
3'b000: ALUfunc = 3'b000; // add
3'b001: ALUfunc = 3'b101; // sll(<<)
3'b010: ALUfunc = 3'b001; // slt(做减法运算)
3'b011: ALUfunc = 3'b001; // sltu(做减法运算)
3'b100: ALUfunc = 3'b100; // xor
3'b101: begin
case(funct7_30)
1'b0: ALUfunc = 3'b111; // srl(>>>)
1'b1: ALUfunc = 3'b110; // sra(>>)
endcase
end
3'b110: ALUfunc = 3'b011; // or
3'b111: ALUfunc = 3'b010; // and
endcase
end
3'b010: ALUfunc = 3'b000; // load类指令 add
3'b011: ALUfunc = 3'b000; // store类指令 add
3'b100: ALUfunc = 3'b001; // branch类指令 sub
3'b101: ALUfunc = 3'b000; // jump类指令 add
3'b110: ALUfunc = 3'b000; // U-type指令 add
default: ALUfunc = 3'b000;
endcase
end
endmodule

ALU_A

选择AluA的数据来源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
module ALU_A(
input logic [3:0] ALUop,
input logic [6:0] OpCode,
input logic [63:0] ReadData1,
input logic [63:0] PCaddress,
output logic [63:0] AluA
);
always_comb begin
case(ALUop)
3'b000: AluA = ReadData1; // R-type
3'b001: AluA = ReadData1; // I1-type
3'b010: AluA = ReadData1; // I2-type
3'b011: AluA = ReadData1; // S-type
3'b100: AluA = ReadData1; // B-type
3'b101: AluA = PCaddress; // J-type
3'b110: begin
if(OpCode == 7'b0110111) begin
AluA = 64'b0; // U-type(lui)
end else if(OpCode == 7'b0010111)begin
AluA = PCaddress; // U-type(auipc)
end
end
default: AluA = 64'b0;
endcase
end
endmodule

ALU_B

选择AluB的数据来源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module ALU_B(
input logic [6:0] OpCode,
input logic ALUSrc,
input logic [63:0] ReadData2,
input logic [63:0] Imm,
output logic [63:0] AluB
);
always_comb begin
if(ALUSrc) begin
if(OpCode == 7'b1101111 || OpCode == 7'b1100111) begin // J-type(jal, jalr)
AluB = 4;
end else begin
AluB = Imm;
end
end else begin
AluB = ReadData2;
end
end
endmodule

ALU

执行算术逻辑运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
module ALU(
input logic [63:0] A,
input logic [63:0] B,
input logic [2:0] ALUfunc,
output logic [63:0] result,
output logic Zero,
output logic SignedLess,
output logic UnsignedLess
);
assign Zero = (result == 64'b0);
assign SignedLess = ($signed(A) < $signed(B));
assign UnsignedLess = (A < B);

always_comb begin
case(ALUfunc)
3'b000: result = A + B;
3'b001: result = A - B;
3'b010: result = A & B;
3'b011: result = A | B;
3'b100: result = A ^ B;
3'b101: result = A << B;
3'b110: result = A >> B;
3'b111: result = A >>> B;
endcase
end
endmodule

DataMem

执行对数据内存的读写操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
module DataMem(
input logic [63:0] Address,
input logic [63:0] WriteData,
input logic MemWrite,
input logic MemRead,
input logic [2:0] funct3,
output logic [63:0] ReadData
);
logic [7:0] mem[0:1023];

always_comb begin
if(MemWrite) begin
case(funct3)
3'h0: mem[Address] = WriteData[7:0];
3'h1: {mem[Address], mem[Address+1]} = WriteData[15:0];
3'h2: {mem[Address], mem[Address+1], mem[Address+2], mem[Address+3]} = WriteData[31:0];
endcase
end
if(MemRead) begin
case(funct3)
3'h0: ReadData = {{(64 - 8){mem[Address][7]}}, mem[Address]};
3'h1: ReadData = {{(64 - 16){mem[Address][7]}}, mem[Address], mem[Address+1]};
3'h2: ReadData = {{(64 - 32){mem[Address][7]}}, mem[Address], mem[Address+1], mem[Address+2], mem[Address+3]};
3'h4: ReadData = {{(64 - 8){1'b0}}, mem[Address]};
3'h5: ReadData = {{(64 - 16){1'b0}}, mem[Address], mem[Address+1]};
endcase
end
end
endmodule

Conditional

根据ALU计算得到的符号位ZereSignedLessUnsignedLess,判断是否跳转,得到CndCode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module Conditional(
input logic Branch,
input logic Zero,
input logic SignedLess,
input logic UnsignedLess,
input logic [2:0] funct3,
output logic CndCode // 是否满足条件跳转
);
always_comb begin
if(Branch) begin
case(funct3)
3'h0: if(Zero) CndCode = 1'b1; // beq
3'h1: if(!Zero) CndCode = 1'b1; // bne
3'h4: if(SignedLess) CndCode = 1'b1; // blt
3'h5: if(!SignedLess) CndCode = 1'b0; // bge
3'h6: if(UnsignedLess) CndCode = 1'b1; // bltu
3'h7: if(!UnsignedLess) CndCode = 1'b1; // bgeu
default: CndCode = 1'b0;
endcase
end else begin
CndCode = 1'b0;
end
end
endmodule

RegBack

选择写回寄存器的数据来源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module RegBack(
input logic MemRead,
input logic [63:0] ReadData, // 从内存中读出的数据
input logic [63:0] ALUresult,
output logic [63:0] WriteData
);
always_comb begin
if(MemRead)begin
WriteData = ReadData;
end else begin
WriteData = ALUresult;
end
end
endmodule

arch

在顶层模块,完成连线。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
module arch(
input logic CLK
);
logic [63:0] PCaddress;
logic [63:0] PCnext;
logic [63:0] PCincrement;

initial begin
PCaddress = 64'b0;
end

PC pc(
.clk(CLK),
.PCnext(PCnext),
.PCaddress(PCaddress)
);

PCincrement pcincrement(
.PCaddress(PCaddress),
.PCincrement(PCincrement)
);

logic [6:0] OpCode;
logic [4:0] Rs1;
logic [4:0] Rs2;
logic [4:0] Rd;
logic [31:0] Inst;
logic [63:0] Imm;

InstMem instmem(
.PCaddress(PCaddress),
.OpCode(OpCode),
.Rs1(Rs1),
.Rs2(Rs2),
.Rd(Rd),
.Inst(Inst)
);

logic RegWrite;
logic [63:0] WriteData;
logic [63:0] ReadData1;
logic [63:0] ReadData2;

RegFile regfile(
.clk(CLK),
.ReadReg1(Rs1),
.ReadReg2(Rs2),
.WriteReg(Rd),
.RegWrite(RegWrite),
.WriteData(WriteData),
.ReadData1(ReadData1),
.ReadData2(ReadData2)
);

ImmGen immgen(
.Inst(Inst),
.Imm(Imm)
);

logic Branch;
logic Jump;
logic JumpReg;
logic MemRead;
logic MemWrite;
logic [2:0] ALUop;
logic ALUSrc;
logic Halt;

Control control(
.OpCode(OpCode),
.Branch(Branch),
.Jump(Jump),
.JumpReg(JumpReg),
.MemRead(MemRead),
.MemWrite(MemWrite),
.ALUop(ALUop),
.ALUSrc(ALUSrc),
.RegWrite(RegWrite),
.Halt(Halt)
);

logic [2:0] ALUfunc;

ALU_control alu_control(
.ALUop(ALUop),
.funct3(Inst[14:12]),
.funct7_30(Inst[30]),
.ALUfunc(ALUfunc)
);

logic [63:0] AluA;
logic [63:0] AluB;
logic [63:0] ALUresult;
logic Zero;
logic SignedLess;
logic UnsignedLess;

ALU_A alu_a(
.ALUop(ALUop),
.OpCode(OpCode),
.ReadData1(ReadData1),
.PCaddress(PCaddress),
.AluA(AluA)
);

ALU_B alu_b(
.OpCode(OpCode),
.ALUSrc(ALUSrc),
.ReadData2(ReadData2),
.Imm(Imm),
.AluB(AluB)
);

ALU alu(
.A(AluA),
.B(AluB),
.ALUfunc(ALUfunc),
.result(ALUresult),
.Zero(Zero),
.SignedLess(SignedLess),
.UnsignedLess(UnsignedLess)
);

logic [63:0] ReadData; // 从内存中读出的数据

DataMem datamem(
.Address(ALUresult),
.WriteData(ReadData2),
.MemWrite(MemWrite),
.MemRead(MemRead),
.funct3(Inst[14:12]),
.ReadData(ReadData)
);

RegBack regback(
.MemRead(MemRead),
.ReadData(ReadData),
.ALUresult(ALUresult),
.WriteData(WriteData)
);

logic CndCode;

Conditional conditional(
.Branch(Branch),
.Zero(Zero),
.SignedLess(SignedLess),
.UnsignedLess(UnsignedLess),
.funct3(Inst[14:12]),
.CndCode(CndCode)
);

PCNext pcnext(
.PCaddress(PCaddress),
.PCincrement(PCincrement),
.Imm(Imm),
.Rs1(ReadData1),
.Jump(Jump),
.JumpReg(JumpReg),
.CndCode(CndCode),
.Halt(Halt),
.PCnext(PCnext)
);

endmodule

正确性验证

为检验正确性,我向 InstMem 中写入了两条指令:

  • lui x5, 0x12345 :0x126802b7

  • add x7, x5, x6:0x005303b3

然后我们在 Vivado 中进行 synthesis 和 stimulation 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module sim();
logic clk;
arch test(.CLK(clk));

initial begin
repeat (20) begin
clk = 0;
#5;
clk = 1;
#5;
end
$finish;
end

initial begin
$dumpfile("test.vcd");
$dumpvars;
end
endmodule

我们可以看到:指令都被正确解析。

波形图

我们查看Regfile中的5号、7号寄存器。它们都被正确地写入和读取。

波形图2

注记

何为计算机网络

定义

目前还没有一个严格的定义,这里可以作如下理解:把分布在不同地理位置上的具有独立功能的多台计算机、终端及其附属设备在物理上互连,按照网络协议相互通信,以共享硬件、软件和数据资源为目的的系统称作计算机网络。


数据交换

数据交换技术一般分为一下三种:

  • 线路交换:通过网络中的结点在两个站之间建立一条专用的通信线路。(比如电话系统。在通话之前,通过用户的呼叫,如果呼叫成功,则从主叫端到被叫端就建立了一条物理通路,这样双方就能进行通话,当通话结束后双方挂机,这时为进行通话所建立起来的物理通路就自动拆除。)
  • 报文交换:对一些实时性要求不高的信息,可以采用报文交换。报文交换方式传输的单位是报文,在报文中包括要发送的正文信息和指明收发站的地址及其它控制信息。在报文交换中, 不需要在两个站之间建立一条专用通路。相反,发送站只要把一个目的地址附加在报文上,然后发送整个报文即可。报文从发送站到接收站,中间要经过多个结点,在这每个中间结点中,都要接收整个报文,暂存这个报文,然后转发到下一个结点。
  • 报文分组交换:是国际上计算机网络普遍采用的数据交换方式。发送站把一个要传送的报文分成若干段,每一段都作为报文分组的数据部分。由于报文分组交换允许每个报文分组走不同的路径,所以一个完整的报文分组还必须包括地址、分组编号、校验码等传输控制信息。

线路交换安全性很高,不易监听和截获数据包;但是相对的成本高。报文交换成本虽低,但是中转站点都接收到完整的数据包,安全性低。报文分组交换则综合前两者的优点,成本低,且每个中转站点都只接受到部分数据包内容,安全性较高。


网络拓扑结构

计算机网络拓扑结构一般有以下六种:

  • 星型拓扑结构
  • 总线型拓扑结构
  • 环形拓扑结构
  • 树型拓扑结构
  • 全互连型拓扑结构
  • 混合型拓扑结构:比较常见的有 星型总线 拓扑和 星型环 拓扑。
拓扑结构 图示
星型拓扑结构
总线型拓扑结构
环形拓扑结构
树型拓扑结构
全互连型拓扑结构
混合型拓扑结构

类型

计算机网络一般分为一下两类:

  • 局域网LAN(local area network)
  • 广域网WAN(wide area network)

网络的体系结构

层次

层次是人们对复杂问题处理的基本方法。将总体要实现的很多功能分配在不同层次中,对每个层次要完成的服务及服务要求都有明确规定。基本地:

  • 不同的系统分成相同的层次
  • 不同系统的最低层之间存在着“物理” 通信
  • 不同系统的对等层次之间存在着“虚拟” 通信
  • 对不同系统的对等层之间的通信有明确的通信规定
  • 高层使用低层提供的服务时,并不需要知道低层服务的具体实现方法

最重要的是 OSI参考模型:它将计算机网络通信划分成了七个层次。(有时为了简化:应用层+表示层+会话层,合称应用层

OSI


接口

接口是同一结点内相邻层之间交换信息的连接点。同一个结点的相邻层之间存在着明确规定的接口,低层向高层通过接口提供服务。(只要接口条件不变、低层功能不变,低层功能的具体实现方法与技术的变化不会影响整个系统的工作


协议

网络协议是为网络数据交换而制定的规则、约定与标准。

计算机如何通信

通俗理解

  • 在现实中,我们如果要给对方写信,除了信件内容以外,我们还需要填写信封。信封有两个重要信息,一个是收件地址,另一个是收件人。收件地址表示这封信要寄到哪里,收件人表示信要寄给谁。同理,在网络世界中,我们如果要发信息给别人,同样需要收件地址和收件人,而这里就是MAC地址和IP地址。MAC地址就是收件地址,IP地址就是收件人
  • 如果把网络比作我们生活的城市,网卡就是我们居住的一栋栋建筑,MAC地址就是这栋建筑的物理地址,而IP地址就是住在这些建筑里的人。不同于写信,在计算机网络中发送信息,我们只需要填写内容和对方的IP地址。操作系统会通过查询ARP表自动获取对方的mac地址,补齐这封信,并从网卡发送出去。


MAC地址

虽然计算机、手机、电视机等是不同类型的电子设备,但它们的通讯都是通过内部的网卡设备来进行。每张网卡在出厂使都被写入一个MAC地址。该地址由6个字节构成:前3个字节是网络硬件厂商编号,后3个字节是该厂商所制造的网卡的序列号。所以每个mac地址都是全球唯一的。


IP地址

IP地址有两种:

  • IPV4地址:由4个0~255的数字构成,用小数点间隔开。ipv4地址理论上有42亿个,但由于早期编码和分配的问题,很多区间的编码实际上被空出或不能使用。ipv4地址也在2011年被全部使用。
  • IPV6地址:128位。

DHCP协议

你可能会困惑:我的电脑接上网线,或是手机接上路由器之后,并没有要求我配置IP地址也能正常使用?这是因为DHCP协议自动帮我们配置了IP地址。

我们有两种方式配置IP地址:

  • 手动配置:自己设置IP地址、子网掩码、默认网关等信息。这种方式的好处在于可以根据自己的规划,设置每台设备的固定IP,有利于网络的统一管理。
  • 动态获取:根据DHCP协议,我们根本不需要关心设备的IP是多少,自动分配一个IP就好了。

粗略地讲。当电脑插上网线或是手机连上wifi,操作系统网络协议栈会自动向外发送一包DHCP请求,请求为其分配IP地址。路由器获取到DHCP请求后,会为其分配一个IP地址,并通过DHCP回复报文发送回去。操作系统收到DHCP回包后,将其分配的IP地址配置到网卡上。注意,在一个 局域网 中,IP地址是唯一的。路由器不会分配重复的IP地址给不同的设备。

完整流程略有不同。计算机通过网线连接到路由器上,当电脑开机进入操作系统后,此时其还没有IP地址。操作系统会使用UDP协议向外广播 DHCP Discover数据包 ,寻找DHCP服务器。网络中的所有设备都能会收到这一数据包,但只有DHCP服务器会做出响应。在家庭网络中,路由器就是DHCP服务器。路由器在自己的IP地址池中选出空余的IP,并决定分配给计算机。路由器会把此IP封装成 DHCP offer包 ,回复给计算机。计算机收到DHCP offer包,需要考虑是否使用该IP。这是因为网络中可能有多个DHCP服务器,它们都收到了计算机的DHCP请求,都为其分配了IP。一般情况下,计算机会使用第一个收到的DHCP offer包。之后,计算机会广播自己的决定,这个数据包称为 DHCP Request包 。路由器接收到request包之后,会回复一个 ACK包 ,表示已接受计算机的选择,并确认此IP可用。

前两步并不是必须的。当计算机申请过IP,计算机重启后,计算机无需重新获取IP地址,只需要再次确认就可以了,也就是从第三步开始。

注意,DHCP协议是应用层协议


ARP协议

我们提到MAC地址是由操作系统来补齐的,那操作系统是怎么知道对方的MAC地址的呢?

在现实中写信,有两个关键的字段需要填写:收件人、收件地址。假设我想给好朋友ter写一封信,但是我不知道收件地址。此时信是寄不到的,怎么办呢?我应该先打电话给ter,询问他的收件地址,再填写到我的信件上。在计算机网络中的通信亦是如此。ARP协议就是帮我们打电话给目标IP并查询它的MAC地址。ARP协议(地址解析协议)就是通过IP地址来查询MAC地址的协议

当计算机A想向计算机B发送信息时:A并不会立即发出,而是先广播一包ARP请求报文,来问一下计算机B的MAC地址是多少。此时网络中的所有设备都收到了这一包请求报文。除了计算机B以外的所有设备都会丢弃这包报文,只有计算机B会回复自己的MAC地址是多少。计算机A收到复后,首先会把计算机B的MAC地址缓存起来,以便下次使用。然后计算机A再将MAC地址添加进报文头部,通过网卡发送出去。

并不是每次发送信息都需查询这个步骤。每次查询到的MAC地址会存入ARP表,以便下次使用。只有ARP表中查无此人人时,才会进行查询。

ARP协议是数据链路层协议。在被广播的ARP请求报文中,网络层的ARP协议中的目标MAC地址会全填0,表示请求。但数据链路层的目标MAC地址则全是1,表示进行广播。

你可能困惑:现实生活中,我的好朋友ter可能搬家,那搬家之后,我照着之前的地址写信不就寄错了吗?计算机网络中也是同理,尽管MAC地址时固定的,但是IP地址是可以修改的。对此,解决方案也很简单:ter主动向所有人广播自己的新地址。

实际上,当计算机修改IP地址后,操作系统会主动向网络中广播一包ARP数据包。此数据包不需要回复,目的是告诉网络中的所有其他主机:当前IP地址和MAC地址的绑定关系。每一台收到ARP数据包的主机,会更新自己的ARP表。


ARP攻击

假设我们有如下网络。无论是TCP协议还是UDP协议,主机A若想与互联网通信,都要通过路由器。亦即:主机A上的所有数据包的数据链路层的目标MAC地址都是路由器的MAC地址。当然,这个MAC地址也是通过ARP协议查询网关IP得到的。

假设此时网络中的另一台主机C,向网络中广播一包ARP报文。它在报文中伪造其IP地址是网关地址,MAC地址是主机C的MAC地址。这相当于告诉网络中的其他主机:“网关IP在我这里!”于是其他所有主机会更新各自的ARP表,并认为主机C是网关,把数据包发送到主机C。于是,整个网络中的主机都无法连接互联网。这就是ARP攻击

网络间如何通信

子网划分

我们将IP地址与子网掩码按位相与。如果两个IP地址 与子网掩码相与的结果 相同,就认为它们在同一子网中。

由于子网掩码都是由连续的1和连续的0组成,我们常用1的数量来表示子网掩码。因此,我们用IP/子网掩码来表示一个网络。(常用的子网掩码如255.255.255.0)

如果想扩大网络中IP地址数量,只需调小子网掩码,亦即减少子网掩码中1的个数。反之,如果想缩小网络中IP地址数量,只需调大子网掩码,亦即增加子网掩码中1的个数。(比如:192.168.1.0/24这个网络中有255个IP地址)


网关

根据TCP/IP协议,不同子网之间是不能直接通信的。如果要通信,则需要通过网关来进行转发。

网关上有两张网卡,分别配置了属于两个不同子网的IP地址,从而可以在两个网络间转发数据包。

举一个简单的例子。子网1中的计算机a发送数据包时,首先会根据目标IP地址,判断其是否与自己处于同一子网。如果属于同一子网,则直接从计算机a的网卡发出。若不是同一子网,则需要把数据包的目标MAC地址改为网关MAC地址,然后发送给网关。网关拿到数据包后,查询 路由表 ,知道这包数据属于子网2。网关把目标MAC地址修改计算机b的MAC地址,把源MAC地址修改成自己的MAC地址。然后网卡把数据包从子网2的网卡发出。以上根据目标IP判断如何发送的行为,我们称之为“路由”。


交换机

交换机负责把数据包发送到正确的位置。在写信的例子中,交换机相当于邮递员。交换机根据数据包中的目标MAC地址,找到它对应的物理端口。一台交换机有很多个端口,它们有自己的编号。计算机的网卡通过网线连接到交换机的网口上,这样每个端口都是一个确定的物理位置。我们只需要知道某个MAC地址在哪个端口上,就能正确地把数据包发给目标MAC地址。所以在交换机中,有一张端口与MAC地址地映射关系表,交换机维护这个映射关系。

每一包数据都有两个MAC地址:源MAC地址(发送方)、目标MAC地址(接收方)。交换机收到一包数据后,首先把这包数据的源MAC与接受端口进行绑定,填入MAC地址表。然后交换机查找目标MAC地址,确定从哪个端口发送出去(发出端口)。如果MAC地址表中查询到了关联端口,则直接从关联端口发出。若没有查找到关联端口,则向除了接受端口外的所有端口群发。这种行为称之为“泛洪”。如果目标MAC地址在这个网络中,则它一定能收到群发的数据包。如此运行一段时间后,交换机的MAC地址表就涵盖网络中的所有网卡设备。由此可见,交换机只关心MAC地址,不关心IP地址

MAC地址在TCP/IP协议中处于第二层(数据链路层),因此交换机通常也称为二层设备。


路由器

如果忽略路由器的WAN口,那么路由器其实就是一台交换机

路由器有两种接口:

  • LAN口:可以多个,用于连接家庭网络设备(比如台式机、手机、笔记本,其中手机、笔记本是通过wifi连接到路由器的)。
  • WAN口:只有一个,连接到运营商网络,从而连接到互联网中。

路由器通过LAN口接入内网,通过WAN口接入互联网。它们属于两个不同的子网。所以,从内网访问互联网就是跨网络的行为,这就需要路由器担任网关的角色


SNAT技术

尽管同一子网中的IP地址互不相同,但是不同子网中的IP地址可能相同。那么当不同家庭内网中的两台计算机拥有相同的IP时,如果它们同时访问互联网,就会造成IP冲突,使得ARP表混乱,使得通信目标变得不确定。为了解决这一问题,我们采用了源地址转换技术(SNAT)

首先计算机发送一包数据,到达路由器。在这包数据的网络层,有两个重要信息:源IP、目标IP。路由器执行SNAT,将源IP修改成路由器WAN口的公网IP,再将数据包发送到服务器。服务器收到数据包后进行处理,在发送数据包到路由器,以进行回复。路由器再进行反向SNAT操作,把目标IP修改成计算机a的IP。

这是简化的网络通信模型,真实情况会更复杂一些。

假设同一子网中两台计算机同时访问同一个服务器,服务器会回复两个数据包分别给两台计算机。但两个数据包的源IP都是服务器IP,目标IP都是路由器的公网IP。那么,路由器如何判断两个数据包分别该给谁呢?

所以,只关注IP地址(网络层)是不够的,还需要其他确定性标记。这是就需要把关联的属性扩展到下一层(传输层)。

我们以最常见的 传输层协议TCP协议 为例。TCP协议有两个关键的属性:源端口、目标端口。这时我们的SNAT技术就变成:修改源IP地址和源端口,并将 修改后的端口号源IP+源端口 形成映射关系。

除了TCP协议以外, UDP协议 也是同理:IP地址+端口号。但是ping命令(常用来检查目标联通性)采用的是 ICMP协议 。它没有端口信息,则需要使用协议中的type+code,来代替端口进行关联。


DNAT技术

DNAT名为目标地址转换。顾名思义,DNAT是修改目标地址。

如果我们的内网计算机对外提供服务,公网上发送来的请求不能直接到达内网计算机。我们就需要用DNAT技术向内网转发请求。

假设我们内网的一台主机上有一个web服务,正在监听某个端口。我们需要在路由器上配置一个DNAT,作用为:访问 公网IP的某个端口 就转接到 主机IP的对应端口 。则当返回问数据包从WAN口进入后,路由器执行DNAT:修改目标IP、目标端口

DNAT技术和SNAT技术统称为NAT技术,将网络一层层分隔开,同时实现不同网络间的通信。


ICMP协议

ping命令使用ICMP协议,全称:互联网控制消息协议。其作用:检测网络中的各种问题,然后做出诊断和解决。它有两大功能:

  • 询问报告:询问目标主机是否可以连通,例如ping命令。
  • 差错报告:当目标网络、或目标端口不可达时,向主机报告错误,例如traceroute命令。

ping命令执行流程:

  • 记录当前时间,并构建ICMP报文。ICMP报文关键字段如下图。其中请求报文的ICMP类型是8。

  • 操作系统会发送ICMP查询报文。如果目标存在,目标主机会构造一包回复报文,并发送会源主机。回复报文的ICMP类型为0。

  • 源主机接收ICMP回复报文。(也可能没有收到回复)

  • 再次记录当前时间,并与之前记录的时间做差,计算得到延迟时间。

注意:如果目标主机不存在、或回复报文丢失:源主机则会等待一段时间后超时,报告目标主机无法连通。

ping命令只能检测两台主机是否连通。而traceroute命令可以知道从源主机到目标主机之间经历了哪几个网关的转发、以及这些网关的IP地址。因此,traceroute指令可以帮助我们了解复杂网络的拓扑结构。

traceroute如何做到的呢?

在网络层的IP数据包头中,有一个TTL字段:表示此帧数据可经过的最大节点数。假设发送一帧 TTL = 2 的数据包,则其每经过一个网关,TTL减少1。它到达第二个网关时,TTL = 0。此时网关不能再向前发送数据包,只能丢弃数据包,并通过ICMP协议向源主机报告错误。这即是ICMP的差错报告功能之一。

traceout依次发送TTL = 1、2、3……的 UDP报文 ,源主机通过回复报文依次知道第1、2、3……个网关的IP地址,并计算出其延时。直到TTL为某个值时,UDP报文发送到目标主机。

这时,目标主机既不需要再向前转发,也因TTL = 0而不需要回复ICMP差错报文。那么源主机怎么知道UDP报文已经抵达目标主机呢?

这是由于源主机构造UDP报文时将目标端口设置为一个很大的值,即:一个不存在的目标端口。所以目标主机同样会回复 因目标端口不可达而产生 的ICMP差错报文。源主机收到报文后,即可知道目标主机已到达。

传输层协议

通俗理解

TCP协议UDP协议都工作在传输层,作用都是在程序之间传输数据。数据可以是文本文件、视频、图片。对TCP和UDP来说,数据都是一堆二进制数,并无区别。TCP和UDP之间最大的区别在于:TCP基于连接,UDP基于非连接

做一个简单的比喻。如果把人与人之间的通信比喻为进程与进程之间的通信,我们基本有两种方式:写信、打电话。TCP是打电话,UDP则是写信。写信时,我们关心:对方是否收到、内容是否完整、顺序是否正确(当我们寄出多封信时)。但这些都是无法确定的,甚至我们都无法确认收信人、收信地址是真实存在的。与此不同,打电话的整个流程(电话接通、相互通话、通话挂断)都能得到及时的反馈,并且能确认对方准确的接收到。


TCP 协议

TCP有三个关键步骤:三次握手传输确认四次挥手

三次握手是建立连接的过程。当客户端向服务端发起连接时,客户端会先发一包数据请求连接,询问能否与服务端建立连接。这包数据称为 SYN包 。如果对端同意连接,则回复一包 SYN+ACK包 。客户端收到回复后,再发送 ACK包 ,就能建立连接。因为该过程中相互发送了三包数据,所以称作“三次握手”。

你可能会困惑:为什么要三次握手而不是两次握手呢?这是为了防止已失效的请求报文突然又传到服务器而引起错误。三次握手本质上是为了在不可靠的网络信道上建立起可靠的连接

举一个简单的例子。假设采用两次握手。假设客户端向服务器发送了SYN1包请求建立连接,但由于某种原因(比如在中间的某个网络结点发生了滞留),SYN1包没有发送到服务器。于是客户端会重新发送SYN2包请求建立连接,这次服务器收到并成功地与客户端建立了连接。之后SYN1包又到达了服务端,服务器会以为是客户端希望建立新的连接,于是发送SYN+ACK包,从而服务端在以上两次握手之后进入等待数据状态。服务端认为是2个连接,而客户端认为是1个连接,造成状态不一致。如果是三次握手,服务端最后收不到客户端发送的ACK包,便不会认为连接建立成功。

经历三次握手之后,客户端和服务端都进入数据传输状态。现在有两个问题需要解决:丢包问题(一包数据可能拆成多包数据进行传输)、乱序问题(这些数据包到达的顺序可能不同)。“传输确认”就是为解决这些问题。

以下过程不区分客户端、服务端,两端均采用下述机制。

TCP协议为每一个连接建立一个发送缓冲区。从建立连接后的第一个字节的序列号为0,往后每个字节的序列号就会增加1。发送数据时,取一部分数据组成发送报文,在其TCP协议头中会附带:序列号、数据长度。接收端在收到数据后需要回复确认报文,确认报文中的 ACK = 序列号 + 长度 = 下一包数据的起始序列号 。如此一问一答的发送方式能让发送端确认数据已经被对方接受。发送端也可以一次发送连续的多包数据,接收端只需要回复一次ACK即可。这样发送端可以把数据切割成一系列待发送的碎片,依次发送到对端。根据序列号和长度,对端能重组出完整的数据。即使其中丢失了某些数据包,接收端也可以要求发送端重传,发送 ACK = 丢失序列包其实序列号

四次挥手是关闭连接的过程。客户端和服务端都可以发起关闭连接请求。

假设客户端想关闭连接,它需要向服务端发送一包数据,称为 FIN包 ,表示要终止连接,然后客户端进入 终止等待1状态 。这是第一次挥手。

服务端收到FIN包,回复一包 ACK包 ,表示自己进入了 关闭等待状态 。客户端收到ACK包,进入 关闭等待2状态 。这是第二次挥手。此时服务端还可以发送未发送的数据,客户端也还可以接受数据。

当服务端发送完数据后,服务端发送一包FIN包,进入 最后确认状态 。这是第三次挥手。

客户端收到FIN包后,回复一包ACK包,进入 超时等待状态 。经过超时时间后,客户端关闭连接。而服务端在收到ACK包时,立即关闭连接。

你可能会困惑:为什么客户端需要超时时间等待?这是为了确保对方已收到ACK包。四次挥手也是为了在不可靠的网络链路中实现可靠的连接断开确认

举一个简短的例子。假设客户端发送完最后一包ACK包后就释放了连接。一旦ACK包在网络中丢失,服务端将一直停留在最后确认状态。相反,如果客户端在发送完ACK包之后等待一段时间,这时服务端因为没有收到ACK包而重发FIN包。客户端会重发ACK包并刷新超时时间。


UDP协议

UDP协议发送数据就是简单地把数据包封装一下,然后从网卡发出去。数据包之间并没有状态上的联系。

正因为UDP这种简单的处理方式,UDP的性能损耗少,占用CPU资源也少。但对于丢包问题,UDP协议并不能保证。因此,UDP在传输稳定性上不如TCP。综上,TCP常适用于对网络通讯质量要求较高的场景:传输文件、发送邮件、浏览网页等;UDP则适用于对实时性要求较高,但是少量丢包并无大影响的场景:域名查询、语音通话、视频直播等。

应用层协议

万维网

万维网WWW(World Wide Web),是一个遍布 Internet 的信息储藏所,是一种特殊的应用网络。它通过超级链接,将所有的硬件、软件、数据资源连成一个网络,用户可从一个站点轻易地转到另一个站点,非常方便地获取丰富的信息。

WWW服务的基础是Web页面,每个服务站点都包括若干个相互关联的页面,每个Web页既可展示文本、图形图像和声音等多媒体信息,又可提供一种特殊的链接点。这种链接点指向一种资源,可以是另一个Web页面、文件、Web站点,这样可使全球范围的WWW服务连成一体。这就是所谓的超文本和超链接技术

超文本实际上是一种解决菜单与信息分离的机制,把可选菜单项嵌入文本中的概念称为“ 超文本” 。超文本技术采用指针连结的网状交叉索引方式,对不同来源的信息加以链接。亦即:一个超文本文件,含有多个指针,而指针可以指向任何形式的文件。正是这些指针指向的“纵横交错”,使得分布在本地的和远程的服务器上的文本文件连接在一起。

你可能会混淆:因特网(Internet)和万维网(Web)。它们有何不同呢?Internet本质上是一种广域网,连接着全球计算机。Internet支持各种各样的服务,其中一项则是Web服务。Web使用URL来定位资源,并用HTML语言渲染出网页


URL

当我们进入一个网页,地址栏就会出现一连串英文字母。它是什么呢?很多人称其为 网址 ,但这并不准确。这其实是 URL

URL指统一资源定位符(Uniform Resource Locator),它是用于标识和定位互联网上的资源的地址。URL 包含了访问资源所需的信息,通常包括:

  • 协议类型:以双斜杠为分隔符,常用的有:”http” 、”https”。协议部分可以省略,浏览器默认“https”。
  • 主机名:即网址。例如 :”www.google.com"、“101.188.67.134”
  • 端口号port 是服务器在其主机所使用的端口号。一般情况下端口号不需要指定,因为通常这些端口号都有一个默认值。只有当服务器所使用的端口号不是默认的端口号时才需要指定。
  • 路径
  • 查询参数等


HTTP协议

HTTP(Hyper Text Transfer Protocol)超文本传输协议是万维网客户端进程与服务器端进程交互遵守的协议。它是一个应用层的协议,使用TCP连接进行可靠的传输。 HTTP是万维网上资源传送的规则,是万维网能正常运行的基础保障。

HTTP的思想非常简单:客户给服务器发送请求, 服务器向客户发送响应, 在客户和服务器之间的HTTP事务有两种类型:请求响应


DNS协议

我们知道,IP地址才是每台计算机或服务器的唯一标识,但日常中我们更多地通过域名来访问服务器。计算机是如何通过域名知道IP地址的呢?这就是DNS协议(Domain Name System)。

通俗地讲。我们每天都用手机进行通信,打电话或者发短信。我们的联系人列表中可能有成千上万个手机号,所以记忆全部手机号是很困难的。于是我们就给每个手机号取名或写备注,比如我的好朋友ter的手机号是54250,则我就给54250取名为ter。下次打电话时,我只需要输入ter,而不需要输入54250,就能通信。这其中,电话号码就相当于IP地址,联系人名称就相当于域名

当我们在浏览器中输入“Google.com”时,浏览器会解析这段网址,从中取出域名,然后组建一包DNS查询报文,并发送到主机的上一级DNS服务器。在开启DHCP协议时,DNS服务器IP地址是会自动获取的。在收到报文后,DNS服务器会在缓存的DNS池中查找域名“Google.com”的记录。如果查无此名,则会再向其更上一级的DNS服务器发送查询报文。最后DNS服务器会返回查找到的IP地址,或者返回“查询失败”。

浏览器并不是每次输入域名都需要查询其IP。它会缓存DNS记录,并在一定时间内直接使用这些记录(只有超过一定时间后,才会再次查询)。

DNS服务器有复杂的上下级关系、结构。这里就不展开。


shell 使用

什么是shell

如今的计算机有着多种多样的交互接口让我们可以进行指令的的输入,如:图像化的用户界面(GUI)、语音输入 、AR/VR 。 这些交互接口可以覆盖 80% 的使用场景,但是它们也从根本上限制了你的操作方式——你不能点击一个不存在的按钮或者是用语音输入一个还没有被录入的指令。为了充分利用计算机的能力,我们不得不回到最根本的方式,使用文字接口:Shell。

shell 通常指的是操作系统提供给用户与内核进行交互的用户接口。Bourne Again Shell,简称 bash。 这是被最广泛使用的一种 shell,它的语法和其他的 shell 都是类似的。


shell 大致原理

shell 基于空格分割命令并进行解析,然后执行第一个单词代表的程序,并将后续的单词作为程序可以访问的参数。如果您希望传递的参数中包含空格(例如一个名为 My Photos 的文件夹),您要么用使用单引号,双引号将其包裹起来,要么使用转义符号 \ 进行处理(My\ Photos)。

类似于 Python 或 Ruby,shell 是一个编程环境,所以它具备变量、条件、循环和函数。当你在 shell 中执行命令时,您实际上是在执行一段 shell 可以解释执行的简短代码。如果你要求 shell 执行某个指令,但是该指令并不是 shell 所了解的编程关键字,那么它会去咨询 环境变量 $PATH,它会列出当 shell 接到某条指令时,进行程序搜索的路径。随后它便会在 $PATH 中搜索由 : 所分割的一系列目录,基于名字搜索该程序。当找到该程序时便执行。


基础指令

  • 查看时间:date

  • 打印文本或变量:echo

  • 查看指定目录下所有文件:ls

  • 查看命令手册:man

    注意:

    1. 通常,在执行程序时使用 -h--help 标记可以打印帮助信息,以便了解有哪些可用的标记或选项。
    2. 如果您想要知道某个命令(程序)怎么用,请试试 man 这个程序。它会接受一个程序名作为参数,然后将它的文档(用户手册)展现给您。

在shell中导航

  • 当前工作目录:pwd

  • 切换目录:cd

  • 查找命令(程序)的路径:which

    注意:

    1. 在 Linux 和 macOS 上,路径使用 / 分割,而在Windows上是 \。路径 / 代表的是系统的根目录,所有的文件夹都包括在这个路径之下;在Windows上每个盘都有一个根目录(例如: C:\)。
    2. *在 Linux 文件系统中,如果某个路径以 / 开头,那么它是一个 绝对路径,其他的都是 相对路径。相对路径是指相对于当前工作目录的路径。在路径中,. 表示的是当前目录,而 .. 表示上级目录:*
    3. 如果命令在 PATH 中找不到,which 通常不会输出任何内容。

文件操作

  • 创建空文件:touch
  • 移动文件 或 重命名:mv
  • 拷贝文件:cp
  • 新建文件夹:mkdir
  • 显示文件内容 或 连接文件:cat
  • 删除文件:rm
  • 查看文件末尾内容:tail

重定向

在 shell 中,程序有两个主要的“”:它们的输入流输出流。 当程序尝试读取信息时,它们会从输入流中进行读取,当程序打印信息时,它们会将信息输出到输出流中。 通常,一个程序的输入输出流都是你的终端。也就是,你的键盘作为输入,显示器作为输出。 但是,我们也可以重定向这些流!

最简单的重定向是 < file> file。这两个命令可以将程序的输入输出流分别重定向到文件:

1
2
3
4
(Py) keats@OMEN-Yanxu:~/Avalon$ touch hello.txt
(Py) keats@OMEN-Yanxu:~/Avalon$ echo "hello world!" > hello.txt
(Py) keats@OMEN-Yanxu:~/Avalon$ cat hello.txt
hello world!

你还可以使用 >> 来向一个文件追加内容(将命令的输出追加到文件的末尾,而不是覆盖文件内容)。

使用管道( pipes,我们能够更好的利用文件重定向。 | 操作符允许我们将一个程序的输出和另外一个程序的输入连接起来:

1
2
3
4
(Py) keats@OMEN-Yanxu:~/Avalon$ ls
ICS IGEM PJ Research vpn.yaml
(Py) keats@OMEN-Yanxu:~/Avalon$ ls -l | tail -n1
-rw-r--r-- 1 keats keats 504 Jan 26 12:35 vpn.yaml

根用户

对于大多数的类 Unix 系统,有一类用户是非常特殊的,那就是:根用户(root user)。 你应该已经注意到了,在上面的输出结果中,根用户几乎不受任何限制,他可以创建、读取、更新和删除系统中的任何文件。 通常在我们并不会以根用户的身份直接登录系统,因为这样可能会因为某些错误的操作而破坏系统。 取而代之的是我们会在需要的时候使用 sudo 命令。顾名思义,它的作用是让您可以以 su(super user 或 root 的简写)的身份执行一些操作。 当您遇到拒绝访问(permission denied)的错误时,通常是因为此时您必须是根用户才能操作。

shell 脚本

什么是 shell 脚本

shell 脚本是一系列用 shell 语言编写的命令,它们按照特定的顺序组织在一个文件中,以执行特定的任务或自动化操作。shell 脚本通常用于执行一系列的命令、条件判断、循环等操作,为用户提供一个方便的方式来批量处理任务。


变量赋值

  • 为变量赋值:foo=bar

  • 访问变量中存储的数值: $foo

    注意:

    1. foo = bar (使用空格隔开)是不能正确工作的。因为解释器会调用程序foo 并将 =bar作为参数。 总的来说,在shell脚本中使用空格会起到分割参数的作用,有时候可能会造成混淆,请务必多加检查。
    2. Bash中的字符串通过'"分隔符来定义,但是它们的含义并不相同。以'定义的字符串为原义字符串,其中的变量不会被转义,而 "定义的字符串会将变量值进行替换。
1
2
3
4
5
6
7
8
9
(Py) keats@OMEN-Yanxu:~/Avalon$ foo=bar
(Py) keats@OMEN-Yanxu:~/Avalon$ echo foo
foo
(Py) keats@OMEN-Yanxu:~/Avalon$ echo $foo
bar
(Py) keats@OMEN-Yanxu:~/Avalon$ echo '$foo'
$foo
(Py) keats@OMEN-Yanxu:~/Avalon$ echo "$foo"
bar

特殊变量

和其他大多数的编程语言一样,bash也支持**if, case, whilefor** 这些控制流关键字。同样地, bash 也支持函数,它可以接受参数并基于参数进行操作。下面这个函数是一个例子,它会创建一个文件夹并使用cd进入该文件夹。

1
2
3
4
mcd () {
mkdir -p "$1"
cd "$1"
}

与其他脚本语言不同,bash使用了很多特殊的变量来表示参数、错误代码和相关变量。常见的有:

  • $0 - 脚本名
  • $1$9 - 脚本的参数。 $1 是第一个参数,依此类推。
  • $@ - 所有参数
  • $# - 参数个数
  • $? - 前一个命令的返回值
  • $$ - 当前脚本的进程识别码
  • !! - 完整的上一条命令,包括参数。常见应用:当你因为权限不足执行命令失败时,可以使用 sudo !!再尝试一次。
  • $_ - 上一条命令的最后一个参数。如果你正在使用的是交互式 shell,你可以通过按下 Esc 之后键入 . 来获取这个值。

输入输出

一般情况下,每个 Unix/Linux 命令运行时都会打开三个文件:

  • **标准输入文件(stdin)**:stdin的文件描述符为0,Unix程序默认从stdin读取数据。
  • **标准输出文件(stdout)**:stdout 的文件描述符为1,Unix程序默认向stdout输出数据。
  • **标准错误文件(stderr)**:stderr的文件描述符为2,Unix程序会向stderr流中写入错误信息。

默认情况下,command > file 将 stdout 重定向到 file,command < file 将 stdin 重定向到 file。

  • 如果希望 stderr 重定向到 file,可以这样写:

    1
    $ command 2>file	# 2表示标准错误文件(stderr)
  • 如果希望将 stdout 和 stderr 合并后重定向到 file,可以这样写:

    1
    $ command > file 2>&1	# 2>&1 表示将 标准错误2 重定向到与 标准输出1 相同的位置
  • 如果希望对 stdin 和 stdout 都重定向,可以这样写:

    1
    $ command < file1 >file2	# <和>后面是否空格不影响命令执行

返回值(退出码)

命令通常使用 STDOUT来返回输出值,使用STDERR 来返回错误及错误码,便于脚本以更加友好的方式报告错误。 返回码或退出状态是脚本/命令之间交流执行状态的方式。返回值0表示正常执行,其他所有非0的返回值都表示有错误发生。

退出码可以搭配 &&(与操作符)和 ||(或操作符)使用,用来进行条件判断,决定是否执行其他程序。它们都属于短路运算符。同一行的多个命令可以用;分隔。程序 true 的返回码永远是0false 的返回码永远是1

1
2
3
4
5
6
7
8
(Py) keats@OMEN-Yanxu:~/Avalon$ false || echo "Oops, fail"
Oops, fail
(Py) keats@OMEN-Yanxu:~/Avalon$ true || echo "Will not be printed"
(Py) keats@OMEN-Yanxu:~/Avalon$ true && echo "Things went well"
Things went well
(Py) keats@OMEN-Yanxu:~/Avalon$ false && echo "Will not be printed"
(Py) keats@OMEN-Yanxu:~/Avalon$ false ; echo "This will always run"
This will always run

另一个常见的模式是以变量的形式获取一个命令的输出,这可以通过 命令替换(command substitution)实现。下面是一个例子:

1
2
(Py) keats@OMEN-Yanxu:~/Avalon$ echo "Starting program at $(date)"
Starting program at Sun Feb 4 16:35:02 CST 2024
  • 当你通过 $( CMD ) 这样的方式来执行CMD 这个命令时,它的输出结果会替换掉 $( CMD )

    例如,如果执行 for file in $(ls) ,shell首先将调用ls ,然后遍历得到的这些返回值。

还有一个冷门的类似特性是 进程替换(process substitution)。这在我们希望返回值通过文件而不是STDIN传递时很有用

  • <( CMD ) 会执行 CMD 并将结果输出到一个临时文件中,并将 <( CMD ) 替换成临时文件名。。

例如, diff <(ls foo) <(ls bar) 会显示文件夹 foobar 中文件的区别。


通配

当执行脚本时,我们经常需要提供形式类似的参数。bash使我们可以轻松的实现这一操作,它可以基于文件扩展名展开表达式。这一技术被称为shell的 通配(globbing)

  • 通配符:当你想要利用通配符进行匹配时,你可以分别使用 ?* 来匹配一个或任意个字符。

    1
    2
    3
    # 对于文件foo, foo1, foo2, foo10 和 bar
    $ rm foo? # 删除 foo1 和 foo2
    $ rm foo* # 删除除了`bar`之外的所有文件
  • **花括号{}**:当你有一系列的指令,其中包含一段公共子串时,可以用花括号来自动展开这些命令。这在批量移动或转换文件时非常方便。


shebang

注意,脚本并不一定只有用 bash 写才能在终端里调用。比如说,这是一段 Python 脚本,作用是将输入的参数倒序输出:

1
2
3
4
#!/usr/local/bin/python
import sys
for arg in reversed(sys.argv[1:]):
print(arg)

内核知道去用 python 解释器而不是 shell 命令来运行这段脚本,是因为脚本的开头第一行的 shebang

shebang 行中使用 env 命令是一种很好的实践,它会利用环境变量中的程序来解析该脚本,这样就提高来您的脚本的可移植性。env 会利用我们第一节讲座中介绍过的PATH 环境变量来进行定位。 例如,此处使用了env的shebang看上去时这样的#!/usr/bin/env python


shell 函数 VS 脚本

初次接触的小伙伴们可能有些疑惑,我们敲进命令行的一条条函数和脚本有什么区别呢?粗略地看,有如下一些不同点:

  • 函数只能与shell使用相同的语言,脚本可以使用任意语言。因此在脚本中包含 shebang 是很重要的。
  • 函数仅在定义时被加载,脚本会在每次被执行时加载。这让函数的加载比脚本略快一些,但每次修改函数定义,都要重新加载一次。
  • 函数会在当前的shell环境中执行,脚本会在单独的进程中执行。因此,函数可以对环境变量进行更改,比如改变当前工作目录,脚本则不行。脚本需要使用 export将环境变量导出,并将值传递给环境变量。
  • 与其他程序语言一样,函数可以提高代码模块性、代码复用性并创建清晰性的结构。shell脚本中往往也会包含它们自己的函数定义。

shell 工具

查看命令如何使用

man命令是手册(manual)的缩写,它提供了命令的用户手册。


查找文件

所有的类UNIX系统都包含一个名为 find的工具,它是 shell 上用于查找文件的绝佳工具。find命令会递归地搜索符合条件的文件。

1
2
3
4
# 查找所有名称为src的文件夹
$ find . -name src -type d
# 查找前一天修改的所有文件
$ find . -mtime -1

除了列出所寻找的文件之外,find 还能对所有查找到的文件进行操作。这能极大地简化一些单调的任务。

1
2
3
4
# 删除全部扩展名为.tmp 的文件
$ find . -name '*.tmp' -exec rm {} \;
# 查找全部的 PNG 文件并将其转换为 JPG
$ find . -name '*.png' -exec convert {} {}.jpg \;w

查看代码

很多类UNIX的系统都提供了grep命令,它是用于对输入文本进行匹配的通用工具。

grep 有很多选项,这也使它成为一个非常全能的工具。其中我们经常使用的有:

  • -C :获取查找结果的上下文(Context)。
  • -v :将对结果进行反选(Invert),也就是输出不匹配的结果。
  • -R :递归地进入子目录并搜索所有的文本文件。

举例来说, grep -C 5 会输出匹配结果前后五行。


查找 shell 命令

随着你使用shell的时间越来越久,你可能想要找到之前输入过的某条命令。

  • 向上的方向键会显示你使用过的上一条命令,继续按上键则会遍历整个历史记录。

  • history 命令允许您以程序员的方式来访问shell中输入的历史命令。这个命令会在标准输出中打印shell中的里面命令。如果我们要搜索历史记录,则可以利用管道将输出结果传递给 grep 进行模式搜索。 例如:history | grep find 会打印包含find子串的命令。

  • 对于大多数的shell来说,你可以使用 Ctrl+R 对命令历史记录进行回溯搜索。敲 Ctrl+R 后您可以输入子串来进行匹配,查找历史命令行。

更多

详见:missing semester

常用 Git 指令

基础设置:

  • 查看所有配置:git config -l

  • 查看系统配置:git config --system --listgit config --system -l

  • 查看用户配置:git config --global --list


  • 设置用户名和邮箱:git config --global user.name "你的用户名"

    git config --global user.email "你的邮箱地址"

  • 删除用户名和邮箱:git config --global --unset user.name

    git config --global --unset user.email


常规操作:

  • 查看所有文件状态:git status

  • 查看指定文件状态:git status [filename]

  • 查看日志:git log

  • 查看引用记录:git relog


  • 初始化(在当前目录下新建一个代码库):git init

  • 添加文件到暂存区:git add [文件名]

  • 添加工作区所有文件到暂存区:git add .

  • 提交更改:git commitgit commit -m "消息内容"

  • 推送到远程仓库:git pushgit push [远程仓库名称] [本地分支名称]:[远程分支名称]

  • 克隆仓库:git clone [URL]

  • 拉取远程变更:git pullgit pull [远程仓库名称] [远程分支名称]

  • 获取远程变更:git fetch [远程仓库名称]

    git fetch会获取最新的提交,但不会自动合并到当前的工作分支


分支管理:

  • 查看所有分支:git branch

  • 创建新的分支:git branch [new branchname]

  • 重命名分支:git branch -m [old branchname] [new branchname]

  • 删除分支:git branch -d [branchbname]

  • 切换分支:git checkout [branchname]git switch [branchname]

  • 合并分支:git merge [source branchname]

    请在目标分支执行git merge,使目标分支同步源分支的更改


  • 撤销修改:git checkout .

    回到最后一次提交的状态,撤销后续的修改。

  • 回退到某次提交:git checkout [commit-hash]

    你不再在任何特定的分支上,而是直接在一个特定的提交上。

    此时,你可以自由地查看这个提交的代码,进行实验性的修改,甚至创建新的提交。这些修改和提交不会影响任何分支,除非你创建一个新的分branch并将这些提交添加到那里。

  • 重置到某次提交:git reset --hard [commit-hash]

    reset会用永久地丢弃后续的提交历史。请确保不再需要之后的提交。


远程仓库:

  • 添加远程仓库:git remote add [远程仓库名称] [远程仓库URL]
  • 查看远程仓库:git remote -v
  • 删除远程仓库:git remote rm [远程仓库名称]

Git 原理

Git 是分布式版本控制。不同于本地版本控制集中式版本控制(如SVN),每个人都拥有全部的代码。所有版本信息仓库全部同步到本地的每个用户,这样就可以在本地查看所有版本历史,可以离线在本地提交,只需在连网时push到相应的服务器或其他用户那里。由于每个用户那里保存的都是所有的版本数据,只要有一个用户的设备没有问题就可以恢复所有的数据,但这增加了本地存储空间的占用。这样不会因为服务器损坏或者网络问题,造成不能工作的情况。(详见:狂神聊Git

Git 本地有三个工作区域,外加远程的git仓库,总共分为四个工作区域:

  • 工作目录 (Working Directory)
  • 暂存区 (Stage/Index)
  • 资源库 (Repository或Git Directory)
  • 远程的 git 仓库 (Remote Directory)

relationship

购买并连接vps服务器

常见服务器卖家有腾讯、阿里、亚马逊,但是这些VPS比较贵。这里我们在Vultr上购买Ubuntu服务器。我们用ssh工具FinalShell连接我们购买的VPS。

finalshell

Shadowsocks原理

Shadowsocks(简称SS)是一种用于科学上网的代理工具,旨在绕过网络审查和限制。它采用 SOCKS5 协议,并使用加密算法来保护数据传输的隐私性。Shadowsocks 的工作原理是通过在本地计算机和远程服务器之间建立一个加密的隧道,将用户的网络流量通过该隧道传输。这样,用户可以访问被封锁的内容或服务,同时保护了通信的隐私性。

Shadowsocks原理

在Ubuntu服务器上安装Shadowsocks

  1. 命令行安装Shadowsocks:apt install shadowsocks-libev

  2. 查看Shadowsocks状态:systemctl status shadowsocks-libev.service

    Shadowsocks状态

    • 这里:enabled表明开机自启,active表示已经启动,目前正在监听8388端口
  3. 编辑配置文件:vim /etc/shadowsocks-libev/config.json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    "server":[ "0.0.0.0"],
    "mode":"tcp_and_udp",
    "server_port":8388,
    "local_port":1080,
    "password":"your password",
    "timeout":86400,
    "method":"chacha20-ietf-poly1305"
    }
    • 这里我是用 vim 编辑配置文件。如果你对 vim 不熟悉,也可以直接在 FinalShell 底部的图形界 面找到 config.json 文件,双击打开,进行编辑。操作上与在 Windows 操作系统上一致。

    • 这里把ip地址改成 0.0.0.0 ,意思是允许所有 ip 地址想 8388 端口发送数据,默认是只允许本机的程序向它发送数据。127.0.0.1是本地的环回地址。

    • 这里的加密方式也是 AEAD ,目前最安全的加密方式。

  4. 重启Shadowsocks:systemctl restart shadowsocks-libev.service

  5. 再查看Shadowsocks状态:systemctl status shadowsocks-libev.service

    Shadowsocks新状态

    • 这里已经是在监听 0.0.0.0 的 8388 端口,就可以进行连接了
  6. 编辑 Clash 的 .yaml 配置文件,然后在 Clash 里尝试连接:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    port: 7890
    socks-port: 7891
    redir-port: 7892
    allow-lan: false
    mode: Rule
    log-level: info
    external-controller: '127.0.0.1:9090'
    secret: ''
    proxies:
    - name: "My Server"
    type: ss
    server: your vps ip
    port: 8388
    cipher: chacha20-ietf-poly1305
    password: "your password"
    proxy-groups:
    - name: Proxy
    type: select
    proxies:
    - "My Server"
    rules:
    - DOMAIN-SUFFIX,google.com,Proxy
    - MATCH,DIRECT
    • 此时会连接超时 Timeout 。这是因为 Ubuntu 服务器上的防火墙 ufw 还没有关
  7. 查看ufw状态:ufw status

    ufw

    • ufw 默认只开放 22 号端口
  8. 开放8388端口:ufw allow 8388

    • 我们可以直接关掉 ufw 防火墙。但这样直接暴露在公网下很危险,所以我们选择新开一个端口 8388 。
  9. 使用Clash连接:

    Clash

  10. 查看Ubuntu服务器日志:journalctl -u shadowsocks-libev.service -f

    • 到第九步为止,我们已经搭建完最简陋的 VPN 了。这条指令可以帮助你发现搭建过程中的错误。