-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.json
301 lines (301 loc) · 196 KB
/
index.json
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
[{
"title": "GDB 调试 Python",
"date": "",
"description": "介绍如何使用 gdb 来调试 Python 程序",
"body": "1. 背景 很多问题在 Python 中很难调试,比如说:\n 段错误(segfaults),这种错误不是未捕获的 Python 异常,而是程序真正发生的段错误 进程 hung 住(hung processes),这种情况下无法获取堆栈信息,也无法通过 pdb 进行调试 守护进程失控(out of control daemon processes) 出现以上 3 种情况,可以使用 gdb 进行调试。\n2. 依赖 使用 gdb 调试 Python 时,需要具体以下条件:\n 安装好 gdb 安装调试 Python 的扩展包,扩展包包含了: debugging symbols Python 特定的 gdb 命令 各个系统安装 gdb 和 python-debuginfo 的方式:\n Fedora sudo yum install gdb python-debuginfo Ubuntu sudo apt-get install gdb python2.7-dbg Centos* sudo yum install yum-utils sudo debuginfo-install glibc sudo yum install gdb python-debuginfo 3. 开始调试 3.1 调试已有进程 命令:gdb python $pid。\n这种情况,会使运行中的进程停止,使用 c 命令可以使其继续运行\n3.2 gdb 启动 Python 进程 命令:\n gdb -ex r \u0026ndash;args python \u0026lt;programname\u0026gt;.py \u0026lt;arguments\u0026gt; gdb python; run \u0026lt;programname\u0026gt;.py \u0026lt;arguments\u0026gt; 这种情况下,程序会一直运行,直到退出、segfaults 或者 Ctrl+C\n3.3 调试-段错误 如果被调试的程序有段错误(segfaulted),gdb 会自动暂停程序,此刻,就可以使用 Python 的 gdb 命令来查看相关信息。\n可以执行如下命令:\n bt:获取 C 的堆栈。如果这里查不到可用信息,继续往下执行; py-bt:获取 Python 堆栈。 3.4 调试-进程 Hung 如果一个进程出现挂起,它要么在等待某个东西(锁、IO等),要么在某个繁忙的循环中。这两种情况下,我们可以使用 gdb attach 到进程,查看堆栈信息。\n如果进程处于繁忙循环中,我们可以继续往下执行代码(使用 cont 命令),然后再次中断(Ctrl+C),然后就可以查看堆栈信息了。\n如果 hung 发生在某个线程,可以执行如下操作:\n 使用 info threads 命令查看所有线程,带星号的线程为当前正在执行的线程 py-list 查看当前线程执行到的代码位置 thread apply all py-list(简写:t a a py-list)查看所有线程的代码位置 frame X 可以用来切换栈帧 参考: 📌 https://wiki.python.org/moin/DebuggingWithGdb\n📌 https://fedoraproject.org/wiki/Features/EasierPythonDebugging\n📌 https://github.com/spyder-ide/spyder/wiki/How-to-debug-Spyder-deadlock-freeze-hang\n📌 gdb 支持的 Python 命令\n",
"ref": "/blog/2022/python/gdb-debugging-python/"
},{
"title": "GO Runtime 内存状态",
"date": "",
"description": "Runtime 中的内存状态字段解析,pprof 中也会输出相关的内存字段供排查问题",
"body": " 官方文档:https://pkg.go.dev/runtime#MemStats\n 0x00 概述 Alloc uint64\n 堆中分配的对象所占用的字节数,与 HeapAlloc 是一样的 TotalAlloc uint64\n 堆中分配的对象所占用的字节数的累积值(释放内存这个数值不会减小) Sys uint64\n 从操作系统获得的内存总字节数,包含 go 运行时的堆、栈和其他数据结构,通常情况下这个值会保持不变 Lookups uint64\n runtime 执行的指针查找次数 Mallocs uint64\n 堆中分配的对象累积的数量,活跃对象计算公式:Mallocs - Frees Frees uint64\n 释放的堆对象的累积计数,活跃对象计算公式:Mallocs - Frees HeapAlloc uint64\n 堆中分配的对象所占用的字节数,与 Alloc 是一样的。(这里包含所有可达的对象和 GC 尚未释放的不可达对象) HeapSys uint64\n 从操作系统获得的堆内存字节数(包含预留的未使用的虚拟地址空间) HeapIdle uint64\n 堆中未使用的字节数(这些内存字节可以返回给 OS,也可以被堆或栈重复使用。HeapIdle-HeapReleased是可以返回给 OS 的内存,但是现在由 runtime 保留,这样就可以不从 OS 申请内存而来增加堆的大小。如果这个值很大,大于堆大小,则表明活跃的堆大小最近出现过瞬时峰值) HeapInuse uint64\n 在使用的堆内存(HeapInuse - HeapAlloc 专用于特定大小的类的内存,但是现在还没有使用。这是碎片的上限值,通常可以有效重用此内存) HeapReleased uint64\n 返回给操作系统的物理内存字节,尚未被堆重新获取的内存字节 HeapObjects uint64\n 在堆中创建的对象数量(随对象的创建和释放而增减) StackInuse uint64\n 栈使用的字节数(没有StackIdle是因为,未使用的栈返回到堆中,因此计入 HeapIdle) StackSys uint64\n 从 OS 获取的栈内存字节数(StackSys = StackInuse + 任何为 OS 线程栈从 OS 申请的内存) GCSys uint64\n GC 元数据使用的字节数 NextGC uint64\n 下一个 GC 周期的目标堆的大小(GC 的目标是保持HeapAlloc ≤ NextGC) LastGC uint64\n 最后一次垃圾收集完成的时间,纳秒的 Unix 时间戳 PauseTotalNs uint64\n 自程序启动以来,GC stop-the-world 的累积纳秒数(在 stop-the-world 暂停期间,所有 goroutine 都会暂停,只有垃圾收集器可以运行。) PauseNs [256]uint64\n 最近的GC stop-the-world 持续的时间,纳秒为单位 PauseEnd [256]uint64\n 最近 GC 暂停结束的纳秒时间戳 NumGC uint32\n GC 循环的次数 NumForcedGC uint32\n 由应用调用 GC 函数所执行的 GC 的次数 GCCPUFraction float64\n GCs 使用的 CPU 的比例 ",
"ref": "/blog/2021/go/go-runtime-memstate/"
},{
"title": "【MIT 6.828】5. 用户环境",
"date": "",
"description": "本节详细学习 6.828 lab 3,操作系统的用户环境",
"body": " lab 3 实验地址:https://pdos.csail.mit.edu/6.828/2018/labs/lab3/\n 0x00 课程介绍 在本实验中将会实现,运行一个受保护的用户模式(user-mode)环境(例如:“进程”等)所需的基本内核功能。我们需要增强 JOS 内核,设计一个数据结构来跟踪用户环境(user environment),创建单用户环境(single user environment),加载一个程序镜像(program image)并运行它。另外 JOS 内核还需要处理用户环境(user environment)发出的任何系统调用(system calls)和造成的任何其他异常(exceptions)。\n 注意:本实验中,术语“环境(environment)”和“进程(process)”的可以互换的,都是一个抽象概念:指允许你运行一个程序。\n引入术语“环境(environment)”而不使用传统术语“进程(process)”,是为了强调 JOS 环境和 UNIX 进程提供不同的接口,并且不提供相同的语义。\n 使用命令 git checkout -b lab3 origin/lab3 切换到 lab3 的最新代码分支,lab3 增加了很多新的源文件,如下所示:\ninc/ env.h 用户模式环境的公共定义 trap.h trap 处理的公共定义 syscall.h 从用户环境到内核的系统调用的公共定义 lib.h 用户模式支持库的公共定义 kern/ env.h 用户模式环境的内核私有定义 env.c 实现用户模式环境的内核代码 trap.h 内核私有 trap 处理定义 trap.c trap 处理代码 trapentry.S 汇编语言 trap 处理程序入口点 syscall.h 系统调用处理的内核私有定义 syscall.c 系统调用实现代码 lib/ Makefrag Makefile 片段来构建用户模式库,obj/lib/libjos.a entry.S 用户环境的汇编语言入口点 libmain.c 从 entry.S 调用的用户模式库设置代码 syscall.c 用户模式 系统调用的存根(stub)函数 console.c 用户模式 putchar 和 getchar 的实现,提供控制台 I/O exit.c 用户模式 退出的实现 panic.c 用户模式 panic 的实现 user/ * 用于检查内核 lab3 代码的各种测试程序 0x01 Part A:用户环境和异常处理 lab3 中新增的文件 inc/env.h 包含了 JOS 中用户环境(user environment)需要使用到的基础定义(definition)。内核使用 Env 数据结构跟踪每一个用户环境(user environment)。本实验中,你只需要创建一个环境,但是需要设计 JOS 内核支持多环境(multiple environments)。lab4 会利用这个特性,来学习多环境交互。\n环境池 ID envid_t 由下图三部分组成,环境索引 ENVX(eid) 等于 envs[] 数组中的环境索引。 Uniqueifier 区分在不同时间创建但共享相同环境索引的环境。所有真实环境都大于 0(因此符号位为零)。envid_ts 小于 0 表示错误。 envid_t == 0 是特殊的,代表当前环境。\n+1+---------------21-----------------+--------10--------+ |0| Uniqueifier | Environment | | | | Index | +------------------------------------+------------------+ \\--- ENVX(eid) --/ 内核维护了三个关于用户环境(user environment)的全局变量:\nstruct Env *envs = NULL; // 维护所有的环境 struct Env *curenv = NULL; // 当前环境 static struct Env *env_free_list; // 空闲环境列表 一旦 JOS 启动并运行,envs 指针将指向一个 Env 结构体的数组(该数组维护系统中所有的环境)。在我们的设计中,JOS 内核最多支持 NENV(#define NENV 1024)个同时活动的环境(active environments),通常情况下,运行环境(running environments)会少的多。envs 数组将包含 Env 结构体的实例,该实例是 NENV(#define NENV 1024)中的某一个可能的环境。\nJOS 内核在变量 env_free_list 中保存所有不活跃(inactive)的 Env 结构体。这种设计使分配(allocation)和取消分配(deallocation)环境变的更简单,因为只需要从变量 env_free_list 中插入或者删除即可。\nJOS 内核使用 curenv 变量来跟踪当前执行的环境。在系统启动期间,在运行第一个环境之前,curenv 被设置为 NULL。\n环境状态 inc/env.h 中定义了 Env 结构体,如下所示(后续 lab 中会增加更多的字段):\nstruct Env { struct Trapframe env_tf // 保存的寄存器 struct Env *env_link; // 下一个空闲的 Env envid_t env_id; // 唯一的环境标识符 envid_t env_parent_id; // 该 env 的父级 env 的 env_id enum EnvType env_type; // 特殊的系统环境 unsigned env_status; // 环境的状态 uint32_t env_runs; // 环境运行的次数 // Address space pde_t *env_pgdir; // 页目录(page dir)的内核虚拟地址 }; env_tf\n Trapframe 结构体定义在 inc/trap.h 中,在当前环境未运行时(例如:当内核或其他环境运行时),维护着当前环境已保存的寄存器的值。内核在从用户模式切换到内核模式时会保存这些信息,以便以后可以在停止的位置恢复环境。 env_link\n 指向 env_free_list 中的下一个空闲 Env。env_free_list 指向列表中的第一个可用空闲环境(free environment)。 env_id\n 该值唯一标识一个环境(envs 数组中使用该值作为唯一标识)。当一个环境终止后,内核将重新分配一个 Env 结构体指向不同的环境,新环境会生成一个新的 env_id,与旧环境的 env_id 保证不相同,即使新环境复用了 envs 数组中的某一项元素(数组中每个元素都是一个插槽(slot))。 env_parent_id\n 内核在此保存了创建当前环境的环境的 env_id。通过这种方式,环境可以形成一个 “family tree”,这有助于做一些安全决策,比如:限制哪些环境可以对谁做什么操作等。 env_type\n 这用于区分特殊环境。对于大多数环境,它将是 ENV_TYPE_USER(ENV_TYPE_USER = 0)。我们将在以后的实验室中为特殊的系统服务环境介绍更多类型。 env_status\n env_status 变量,包含以下值: ENV_FREE:表示当前环境为非活跃(inactive)状态,因此该环境会在 env_free_list 中。 ENV_RUNNABLE:表示该环境正在等待处理器运行。 ENV_RUNNING:表示当前环境正在运行。 ENV_NOT_RUNNABLE:表示该环境处于活跃状态(active),但它当前尚未准备好运行:例如,因为它正在等待来自另一个环境的进程间通信(IPC)。 ENV_DYING:表示该环境是一个僵尸(zombie)环境。僵尸(zombie)环境将在下次捕获到内核时被释放。 env_pgdir\n 此变量保存此环境的页面目录(page directory)的内核虚拟地址。 与 Unix 进程一样,JOS 环境将“线程”(thread)和“地址空间”(address space)的概念结合了起来。线程(thread)主要由保存的寄存器(env_tf 字段)定义,地址空间(address space)由 env_pgdir 指向的页面目录(page directory)和页表(page table)定义。要运行环境,内核必须使用保存的寄存器和适当的地址空间去设置(set up)CPU。\nJOS 内核的 Env 结构体类似于 xv6 中的 proc 结构体。这两种结构体都将环境(或叫做 xv6 中的进程)的用户模式(user-mode)寄存器状态保存在 Trapframe 结构体中。在 JOS 中,各个环境不像 xv6 中的进程那样有自己的内核堆栈。JOS 内核中一次只能有一个活动的 JOS 环境,因此 JOS 只需要一个内核堆栈。\n分配环境数组(envs) 在 lab2 中,使用 mem_init() 为 pages[] 数组分配了内存,内核使用该表跟踪哪些页面是空闲的,哪些页面不是空闲的。现在需要进一步修改 mem_init(),以分配一个 Env 结构体数组,称为 envs。\n创建和运行环境 现在,需要在 kern/env.c 中编写运行用户环境所需的代码。因为我们还没有文件系统,所以我们会设置内核加载一个静态二进制镜像(static binary image,该二进制镜像会嵌入到内核本身)。JOS 将该二进制文件作为 ELF 可执行镜像嵌入内核中。\nlab3 的 GNUmakefile 会在 obj/user/ 目录下生成一些二进制镜像。如果看过 kern/Makefrag 文件,会注意到一些神奇的东西,它们将这些二进制文件直接“链接”(link)到内核可执行文件中,就像它们是 .o 文件一样。链接器命令行上的 -b binary 选项将使这些文件作为“原始”(raw)未解释的二进制文件(uninterpreted binary files)链接,而不是作为编译器生成的常规 .o 文件链接(其实,对链接器而言,这些文件根本不必是 ELF images,它们可以是任何东西,例如文本文件或图片)。如果在构建内核后查看 obj/kern/kernel.sym 文件,可以看到链接器“神奇地”生成了许多具有模糊名称的有趣变量,例如:_binary_obj_user_hello_start、_binary_obj_user_hello_end、_binary_obj_user_hello_size。链接器通过修改二进制文件的文件名来生成这些变量名,这些变量为常规内核代码提供了一种引用嵌入式二进制文件的方法。\n在 kern/init.c 的 i386_init() 函数中,将看到在环境中运行这些二进制镜像的代码。但是,设置用户环境的主要功能还没有完成,需要我们去补充。\n// 在 env.c 文件中,完成以下函数功能: env_init() // 初始化 envs 数组中的所有 Env 结构体,并将它们添加到 env_free_list 中。调用 env_init_percpu 函数,该函数使用 privilege level 0(内核)和 privilege level 3(用户)的单独段(separate segments)来配置分段硬件(segmentation hardware)。 env_setup_vm() // 为新环境分配页目录(page directory),并初始化新环境地址空间的内核部分。 region_alloc() // 为环境分配和映射物理内存。 load_icode() // 解析一个 ELF 二进制镜像,就像引导加载程序( boot loader)已经做的那样,并将其内容加载到新环境的用户地址空间中。 env_create() // 使用 env_alloc 分配环境,并调用 load_icode 将 ELF 二进制文件加载到环境中。 env_run() // 在用户模式下,启动一个给定的环境。 下面是调用用户代码之前的代码调用顺序图,学习下每一步完成了什么动作:\n start (kern/entry.S) i386_init (kern/init.c) cons_init mem_init env_init trap_init (still incomplete at this point) env_create env_run env_pop_tf 处理中断和异常 系统调用 int $0x30 指令是一条死胡同,一旦处理器进入用户模式,就无法返回。我们现在需要实现基本的异常(basic exception)和系统调用处理(system call handling),这样内核就有可能从用户模式代码(user-mode code)中恢复对处理器的控制。我们需要彻底熟悉 x86 中断和异常机制。\n 在本实验室中,我们通常遵循英特尔的中断、异常等术语。然而,诸如异常、陷阱、中断、故障和中止等术语在整个体系结构或操作系统中没有标准意义,并且在使用时通常不考虑它们在特定体系结构(如 x86)上的细微差别。当您在本实验室之外看到这些术语时,其含义可能略有不同。\n 学习 Chapter 9, Exceptions and Interrupts 部分:\n 中断(interrupts)和异常(exceptions)是一种特殊的控制转移(control transfer),它们的工作方式有点像未编程的调用(unprogrammed CALLs) 它们改变正常的程序流以处理外部事件、或报告错误、或异常情况 中断(interrupts)和异常(exceptions)之间的区别在于,中断用于处理处理器外部的异步事件,而异常用于处理处理器在执行指令过程中检测到的条件(conditions) 有两个外部中断源和两个异常源: 中断(这里的硬件中断是通过 CPU 物理层连接两个引脚实现的) 可屏蔽中断(Maskable interrupts),通过 INTR 引脚(pin)发出信号; 不可屏蔽中断(Nonmaskable interrupts),通过 NMI(Non-Maskable Interrupt,不可屏蔽中断)引脚发出信号; 异常 处理器检测(Processor detected),这些进一步分类为断层 faults、traps 和 aborts; 软件触发(Programmed),INTO、INT 3、INT n 和 BOUND 中的指令可以触发异常。这些指令通常被称为“软件中断”(software interrupts),但处理器将其作为异常处理。 个人理解 中断:\n中断(interrupts)是 CPU 和操作系统的最基本特性之一,是支撑多用户、多进程的基础。如果没有中断(interrupts),CPU 会一直工作直到完成你交给它的工作,那么我们就无法同时做多项任务。但是有了中断(interrupts),操作系统就会触发中断(interrupts)来打断 CPU,让 CPU 听下手头的这个事情,去干其他事情,它的切换速度很快,在我们看来就是多项工作在并发来进行。\n异常:\n异常(exceptions)和中断(interrupts)其实是一个道理,都是用来打破 CPU 正常的工作,异常(exceptions)是 CPU 内部触发的,比如说除数为 0 时,CPU 会引发异常(exceptions),然后会转而执行处理异常(exceptions)的指令中去。另外软件也能通过指令来触发“软件中断”(software interrupts),在 CPU 看来就是程序发生了异常(exceptions)。总之:异常(exceptions)和中断(interrupts)的目的都是为了让 CPU 停下来去干其他事情\n信号:\nCPU 可以中断(interrupts),那么进程可以中断(interrupts)吗?比如说进程是个死循环,我想停止进程;或者说进程要接收异步 IO 的消息怎么办?进程也是可以被中断(interrupts),这里使用的是信号,进程会接收操作系统发送出来的信号,通过信号来执行特定的操作。比如说我想停止某个进程,如果是前台进程,我们通过执行 ctrl-c 来终止进程(ctrl-c 其实是一个硬件中断,操作系统收到以后会给进程发送 SIGINT 信号,SIGINT 是让进程终止的信号);如果是后台进程,一般通过 kill -9 pid 来杀死进程(kill -9 pid 原理也是操作系统会发送一个 SIGKILL 信号给进程)\n# 操作系统定义的所有信号 [root@centos ~]# kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX 进程为什么会处理信号,简单理解是因为进程的结构体中有一个 sigbitmap 字段用来保存信号,会处理发出来的信号。可以参考:Linux-进程信号\n 受保护的控制传输 // TODO: 待补充\n异常和中断的类型 // TODO: 待补充\n例子说明 // TODO: 待补充\n嵌套异常和中断 // TODO: 待补充\n设置 IDT(interrupt descriptor table) // TODO: 待补充\n0x02 Part B:页错误、断点异常、系统调用 处理页错误 // TODO: 待补充\n断点异常 // TODO: 待补充 // TODO: 待补充\n系统调用 用户进程通过调用“系统调用”(system calls)请求内核为它们做一些事情。当用户进程调用“系统调用”(system calls)时,处理器进入内核模式(kernel mode),处理器和内核协作保存用户进程的状态,内核执行适当的代码以执行系统调用,然后恢复用户进程。用户进程如何引起内核注意以及它如何指定要执行的调用的确切细节因系统而异。\n在 JOS 内核中,我们将使用 int 指令,这会导致处理器中断(processor interrupt)。特别地是,我们将使用 int $0x30 作为系统调用中断(system call interrupt),可以使用定义好的常量 T_SYSCALL(48=0x30)。所以必须设置中断描述符(interrupt descriptor),以允许用户进程触发该中断。请注意,中断 0x30 不能由硬件生成,因此,允许用户代码生成它不会引起歧义。\n应用程序通过寄存器来传递系统调用编号(system call number)和系统调用参数(system call arguments)。这样,内核就不需要在用户环境的堆栈或指令流中四处搜索。系统调用编号(system call number)会在 %eax 寄存器中,参数(最多五个)将分别位于 %edx、%ecx、%ebx、%edi 和 %esi 寄存器中。内核将返回值传回 %eax 寄存器中。调用“系统调用”(system calls)的汇编代码已经在 lib/syscall.c 中的 syscall() 中编写完成。有兴趣可以看看。\n启动用户模式 // TODO: 待补充\n页面错误和内存保护 // TODO: 待补充\n0xff 参考 MIT 6.828 参考资料\n",
"ref": "/blog/2021/mit/6.828-5/"
},{
"title": "【MIT 6.828】4. 内存管理",
"date": "",
"description": "本节详细学习 6.828 lab 2,操作系统的内存管理",
"body": " lab 2 实验地址:https://pdos.csail.mit.edu/6.828/2018/labs/lab2/\n 0x00 课程介绍 本次实验中,需要写操作系统内存管理的代码(译者注:我就不写了,学习 6.828 的目的是快速了解操作系统,初步学习且时间有限,能看懂就行了)。内存管理包含两大组件:\n 内核的物理内存分配器(physical memory allocator):有了物理内存分配器,内核就可以分配和释放内存空间了。该分配器需要以 4096 字节(称为页,page,#define PGSIZE 4096)为单位来运行。需要做的就是维护一个数据结构,该数据结构记录着哪些物理内存页是空闲的、哪些物理内存页是已分配的、多少进程共享了已分配的内存页。还需要编写申请和释放内存页的代码。 虚拟内存(Virtual Memory):将内核和用户程序使用的虚拟地址映射到物理内存中的地址。x86 硬件的 MMU(内存管理单元,memory management unit)在指令将要使用内存的时候,通过查询页表(page table),进行映射操作,实验中会要求根据给定规范,修改 JOS 中 MMU 的页表(page table)。 接下来的所有实验,将会逐步构建出自己的内核代码。使用命令 git checkout -b lab2 origin/lab2 可以切换到 lab2 的代码分支,lab2 中已经加入了一些相关的源代码。\nlab2 新增了如下文件:\n inc/memlayout.h kern/pmap.c kern/pmap.h kern/kclock.h kern/kclock.c inc/memlayout.h 描述了虚拟地址空间的布局,你需要在 kern/pmap.c 中实现它。inc/memlayout.h 和 kern/pmap.h 定义了 PageInfo 结构体,你需要使用 PageInfo 结构体去跟踪物理内存的哪个页(page)是空闲的。kern/kclock.c 和 kern/kclock.h 控制 PC 的时钟和 CMOS RAM 硬件,在这两个文件里面 BIOS 记录着物理内存的总数和其他内容。kern/pmap.c 代码需要去读取设备硬件来得知它有多少物理内存,但是这部分代码已经写完了,我们暂时不需要关注 CMOS 硬件是如何工作的。\nlab2 需要特别关注 inc/memlayout.h、kern/pmap.h、inc/mmu.h 文件,因为会用到这三个文件中定义的很多结构体。\n0x01 Part 1: Physical Page Management 操作系统必须要跟踪物理 RAM 中哪些是空闲的、哪些是当前正在使用的。JOS 以页面(page)粒度来管理 PC 的物理内存,所以它可以使用 MMU 映射和保护分配的每一块内存。\n现在需要编写物理页分配器(physical page allocator)代码。通过 PageInfo 结构体列表来跟踪哪些页面(page)是空闲的,每个 PageInfo 结构体对应一个物理页面(physical page)。在编写虚拟内存实现之前,需要先编写物理页分配器(physical page allocator)的实现,因为页表(page table)管理代码需要分配物理内存来存储页表(page table)。\nkern/pmap.c 中,必须实现一下函数:\nboot_alloc() mem_init() (only up to the call to check_page_free_list(1)) page_init() page_alloc() page_free() check_page_free_list() and check_page_alloc() test your physical page allocator. You should boot JOS and see whether check_page_alloc() reports success. Fix your code so that it passes. You may find it helpful to add your own assert()s to verify that your assumptions are correct. 要实现上面的函数,可以根据函数的注释和 Intel 手册等资料,还是比较麻烦的。\n页(page)的结构体如下:\nstruct PageInfo { struct PageInfo *pp_link; // 空闲列表中的下一个 page uint16_t pp_ref; // 记录指向该页的指针数量(引用计数) }; 每个 PageInfo 结构体存储着一个物理页的元数据。但是他不是物理页本身,但是物理页和 PageInfo 结构体有一对一的关系,可以使用 kern/pmap.h 中的 page2pa() 函数将结构体 PageInfo 映射到对应的物理地址上。\nPage Directory 和 Page Table 其实就是一个列表,列表中每一项存储着具体的地址信息,目前这种方式相当于二级页表查找。线性地址(32-bit)分为三部分:页目录(10-bit)+ 页表(10-bit)+ 偏移(12-bit)。页目录可以存放 1024(2^10)个页表,每个页表可以存放 1024(2^10)个内存页,每个物理内存页是 4096 字节(4K),所以,整个线性地址可以表示 4G(4K * 1024 * 1024)的内存。如下图所示:\n 物理内存的管理总结起来其实就是一句话:\n物理内存也是使用页(page)来管理,页(page)是由内核的物理内存分配器(physical memory allocator)来划分的,内核中的 PageInfo 结构体和物理页面一一对应。\n 0x02 Part 2: Virtual Memory x86 保护模式(protected-mode)下的内存管理架构:段转换(segment translation)和页面转换(page translation)。\n虚拟地址、线性地址和物理地址 在 x86 的术语中:\n 虚拟地址(virtual address)由段选择器(segment selector)和段内偏移(offset)组成 线性地址(linear address)是在段转换(segment translation)之后,页面转换(page translation)之前得到的地址 物理地址(physical address)是在段转换(segment translation)和页面转换(page translation)之后最终得到的地址,该地址最终会通过硬件总线(hardware bus)来访问 RAM Selector +--------------+ +-----------+ ----------\u0026gt;| | | | | Segmentation | | Paging | Software | |--------\u0026gt;| |----------\u0026gt; RAM Offset | Mechanism | | Mechanism | ----------\u0026gt;| | | | +--------------+ +-----------+ Virtual Linear Physical C 语言中的指针(pointer)其实是虚拟地址中的 “offset”。在 boot/boot.S 文件中,我们安装了一个全局描述符表(GDT,Global Descriptor Table),通过设置所有段基地址(segment base address)为 0,并限制最高到 0xffffffff,有效的禁用了段转换(segment translation)。因此 “selector”(段选择器)无效,线性地址(linear address)始终等于虚拟地址(virtual address)的偏移量(offset)。\n在 lab3 中,我们为了设置权限级别(privilege levels),必须与分段(segmentation)进行更多的交互,但是对于内存转换,我们可以忽略 JOS labs 中的分段(segmentation),只需要关注页面转换(page translation)。\n回想一下 lab1 的 part3,我们使用了一个简单的页表(page table),这样内核就可以在其链接地址(0xf0100000)处运行,尽管它实际加载在 BIOS ROM 上面的物理地址(0x00100000),其中的原因就是由于通过页表(page table)做了一个地址映射,该页表(page table)只能映射 4M 内存。\n本实验中,需要为 JOS 设置虚拟地址空间布局,我们对此进行扩展,映射物理内存的前 256M 内存,虚拟地址从 0xf0000000 开始,并映射虚拟地址空间的其他区域。\n从 CPU 上的执行代码来看,一旦进入保护模式(boot/boot.S 中首先会进入保护模式),就无法直接使用线性地址和物理地址了。所有的内存引用都会被解释为虚拟地址,并由 MMU 进行转换,这意味着 C 语言中的所有指针都是虚拟地址。\nJOS 内核通常将地址作为不透明的值(opaque values)或整数(integers)进行操作,而不去引用他们(例如在物理内存分配器中就是这样使用的)。有时是虚拟地址,有时是物理地址,为了方便区分,JOS 源码分成了两种类型(这两种类型实际上都是 32 位整数类型(uint32_t),因此编译器不会阻止它们相互赋值,由于它们是整数类型而不是指针,如果取消引用它们,编译器会报错):\n physaddr_t 类型表示物理地址 uintptr_t 类型表示虚拟地址 JOS 内核需要先将 uintptr_t 强制转换(cast)成指针类型,才能对 uintptr_t 进行解引用(dereference)操作。相反的,内核不能正确的解引用(dereference)一个物理地址,因为 MMU 转换了所有内存引用。如果将 physaddr_t 强制转换(cast)为指针类型并取消引用,你虽然可以加载和存储得到的地址,但是可能无法获得到预期的内存位置,因为它被视为虚拟地址来被处理的。\nC type Address type T* Virtual uintptr_t Virtual physaddr_t Physical JOS 内核有时候需要直接读取和修改物理地址对应的内存(非虚拟地址对应的内存),例如:向页表(page table)添加映射关系时可能需要申请物理内存来存储页目录(page directory),然后初始化这块内存。但是,内核不能绕过虚拟地址转换这步操作,因此不能直接加载和存储数据到物理地址。原因之一是:JOS 从物理地址 0 的位置重新映射了所有的物理内存到虚拟内存的 0xf0000000 位置,当内核只知道物理地址时,这种方式(物理地址加上 0xf0000000 就是虚拟地址)会帮助内核来读取和写入数据。为了将物理地址转换成内核可以实际读写的虚拟地址,内核必须将 0xf0000000 和物理地址相加,以在重新映射的区域中找到其相应的虚拟地址,可以使用 KADDR(pa) 函数来做加法操作。\nJOS 内核有时还需要找到一个虚拟地址内存对应的物理地址(该地址存储内核的数据结构)。内核的全局变量和被 boot_alloc() 分配的内存处在内核被加载的区域(从 0xf0000000 开始的位置),这块区域包含在映射范围内。因此,要将该区域的虚拟地址转换为物理地址,内核只需要减去 0xf0000000 即可,可以使用 PADDR(va) 函数来做减法操作。\n引用计数(Reference counting) 在后边的 lab 中,会经常看到多个虚拟地址映射到了相同的物理地址,我们需要在 PageInfo 结构体的 pp_ref 字段维护每个物理页面(physical page)被引用的次数。当物理页面(physical page)的引用计数为 0 时,该页就可以被释放了,因为它不再被使用。通常,物理页面(physical page)的引用计数应该等于在所有的页表(page table)范围内 UTOP 以下的区域,该物理页面(physical page)出现的次数。(UTOP 以上的区域大部分是在引导时(boot time)由内核设置的,不应该被释放掉,因此不需要对它们进行引用计数)。我们还将使用引用计数来跟踪指向 page directory 页的指针数量,还有,page directory 对 page table 页的引用数量。\n使用 page_alloc 函数时一定要小心,它返回的页面(page)的引用计数始终为 0,因此当你对返回的页面(page)执行操作(比如将该 page 插入到 page table 中)以后,pp_ref 字段应该立即加 1,有时这个操作是由其他函数(例如,page_insert)处理的,有时调用 page_alloc 的函数必须直接处理。\n页表管理(Page Table Management) 这里需要编写管理页表(page table)的代码:插入和删除线性地址到物理地址的映射、在需要的地方创建 page table 页。\n在 kern/pmap.c 文件中,实现如下函数:\npgdir_walk() boot_map_region() page_lookup() page_remove() page_insert() check_page(), called from mem_init(), tests your page table management routines. You should make sure it reports success before proceeding. 0x03 Part 3: Kernel Address Space JOS 将处理器的 32 位线性地址空间(linear address space)分为两部分,分界线是由 inc/memlayout.h 中 ULIM 符号定义的,会为内核保留大约 256M 的虚拟地址空间:\n 内核会控制上面一部分的(upper part)的布局(layout)和内容 用户环境(user environments)会控制下面一部分(lower part)的布局(layout)和内容 这解释了为什么我们需要在 lab1 中为内核设置如此高的链接地址,如果不设置如此高的链接地址,内核的虚拟地址空间下面就没有足够的空间预留给用户环境。\n权限和故障隔离(Permissions and Fault Isolation) 由于内核和用户内存都存在于每个环境的地址空间中,因此我们必须在 x86 页表(page table)中使用权限位(permission bits),以限制用户代码仅能访问地址空间中的用户部分。否则,用户代码中的错误可能会覆盖内核数据,导致系统崩溃等问题;用户代码还可能窃取其他环境的私有数据。注意:可写权限位 PTE_W 同时影响用户代码和内核代码。\n内核可以读写 ULIM 符号之上的任何内存,用户环境则没有权限访问。[UTOP,ULIM] 地址范围,内核和用户环境具有相同的权限:他们可以读取但是不能写入该地址范围。此地址范围用于以只读方式向用户环境公开某些内核数据结构。最后,UTOP 下面的地址空间供用户环境使用;用户环境将设置访问此内存的权限。\n初始化内核地址空间(Initializing the Kernel Address Space) 现在,我们需要设置 UTOP 上面的地址空间(地址空间中的内核部分)。我们需要使用上面实现的一些函数来设置适当的线性地址到物理地址的映射。\n地址空间布局(Address Space Layout)备选方案 我们在 JOS 中使用的地址空间布局不是唯一的方案。操作系统也可以将内核映射到较低的线性地址,而将上部地址空间留给用户进程。但是,x86 内核通常不采用这种方法,因为 x86 的虚拟 8086 模式(一种向后兼容的模式)在处理器中是“硬连线”的,就是为了使用线性地址空间的底部区域,因此如果内核映射在那里的话,将无法使用。\n甚至可以设计内核,使其不必为自己保留处理器线性或虚拟地址空间的任何固定部分,而是有效地允许用户级进程不受限制地使用整个 4GB 的虚拟地址空间,同时仍然充分保护内核不受这些进程的影响,并保护不同进程彼此不受影响,显而易见,这是非常困难的。\n0xff 参考 MIT 6.828 参考资料\n",
"ref": "/blog/2021/mit/6.828-4/"
},{
"title": "计算机术语【持续更新】",
"date": "",
"description": "收集计算机相关的术语大全",
"body": " 原文 翻译 解释 endpoint uri Web服务中 stub rpc中使用,是一段代码,用来转换客户端与服务器之间传递的参数 prefork 多进程模式,pre 表示在请求来之前子进程已经创建好了(进程池?),一般用在非线程安全的情况下 用户认证 判断是否是合法用户 用户权限 查看合法用户有没有权限 ELF 可执行连接格式(Executable and Linkable Format) 是UNIX系统实验室(USL)作为应用程序二进制接口(Application Binary Interface,ABI)而开发和发布的。扩展名为elf。 GNU GNU's not unix GNU 是追求开源一项运动。Unix 系统被发明以后,大家用的很爽,但是后来闭源开始收费了。一个叫 RMS 的大叔觉得很不爽,于是发起 GNU 计划,模仿 Unix 的界面和使用方式,从头做一个开源的版本。然后他自己做了编辑器 Emacs 和编译器 GCC。做了很多可以运行在 Unix 上的开源软件,但是一直没做出操作系统,于是一个叫 Linus 的博士,写出来 Linux 操作系统,完美符合 GNU 的目的,所以最后 Linux 也纳入了 GNU 中。 AT\u0026amp;T syntax AT\u0026amp;T 是 GNU 汇编语言使用的语法格式,与之相关的是 NASM 汇编语言使用的 Intel 风格的语法 RAM 随机储存器(Random-Access Memory) 断电后数据丢失,也就是电脑上的内存条,程序时放在 RAM 中运行的 ROM 只读储存器(Read-Only Memory) 断电数据不丢失,通俗来讲就是电脑上的硬盘 IA-32 Intel Architecture, 32-bit 的缩写 有时候也叫做 i386 CGI Common Gateway Interface 通用网关接口。早期的 web 服务,后端会绑定一个目录(/usr/local/apache/htdocs/),浏览器请求时,会直接返回目录下 index.html 文件,这就是静态请求,无法满足后来动态请求的发展趋势,所以当浏览器请求 web 服务的特定 URL 时,不直接返回 index.html,而是执行一个脚本,这个脚本的输出会作为 http response 返回给浏览器端,这个脚本就叫做 CGI 脚本。CGI 脚本在执行的时候,还会通过环境变量等形式获取到 http request 带过来的附加信息。慢慢的,CGI 泛指后端服务。 ",
"ref": "/blog/2021/computer-basics/computer-terms/"
},{
"title": "【MIT 6.828】3. PC 启动流程",
"date": "",
"description": "本节学习 6.828 lab 1,PC 启动的详细流程",
"body": " lab 1 实验地址:https://pdos.csail.mit.edu/6.828/2018/labs/lab1/\n 0x00 非官方解读 6.828 实验使用的是 386 CPU,但是 386 CPU 一上电以后,处于 16 位的实模式,与 8086 CPU 很相似,不清楚 8086 的可以学习文章操作系统基础知识概览\nCPU 一上电,CS=0xF000、IP=0xFFF0,地址为 0xFFFF0,这个地址刚好在 BIOS ROM 中,所以先执行 BIOS 中的代码。\nBIOS 执行流程:\n 上电自检(Power On Self Test) 加载磁盘的第一个可引导扇区,一个扇区大小为 512 字节,如何判断该扇区时可引导的?判断最后两个字节为 0xAA55 则表示该扇区可引导 BIOS 会把该扇区加载到段地址为 0x0000,偏移地址为 0x7c00 内存处 然后使用跳转指令跳到 CS=0x0000、IP=0x7c00 处开始执行 至此,BIOS 完成了自己的使命,把执行流交给了 0x7c00 处的代码,所以我们需要做的就是在磁盘的第一个扇区存放我们的代码。 0x01 官方解读(Lab 1: Booting a PC) 实验分为三部分:\n Part 1:主要是熟悉 x86 汇编语言、QEMU x86 仿真器和 PC 的开机引导过程 Part 2:主要是学习 6.828 内核的 boot 引导过程,代码的 boot 目录下 Part 3:主要学习 6.828 中使用的 JOS 的内核初始化模型,代码在 kern 目录下 下载代码:\nathena% mkdir ~/6.828 athena% cd ~/6.828 athena% add git athena% git clone https://pdos.csail.mit.edu/6.828/2018/jos.git lab Cloning into lab... athena% cd lab athena% Part 1: PC Bootstrap x86 汇编 本实验使用的是 GNU 使用的 AT\u0026amp;T 语法的汇编语言,但是你如果实习 Intel语法的汇编语言,那更利于你学习本课程。\n推荐学习书籍《PC 汇编语言》\n模拟 x86 运行 我们不需要在真实的机器上运行\u0026amp;调试我们的程序,那样是在太麻烦了。在 6.828 课程中,我们使用 QEMU 模拟器来模拟 x86 操作系统的运行,另外还可以使用 GDB 来调试。\n执行一下命令,可以模拟 JOS 系统的启动:\nathena% cd lab athena% make + as kern/entry.S + cc kern/entrypgdir.c + cc kern/init.c + cc kern/console.c + cc kern/monitor.c + cc kern/printf.c + cc kern/kdebug.c + cc lib/printfmt.c + cc lib/readline.c + cc lib/string.c + ld obj/kern/kernel + as boot/boot.S + cc -Os boot/main.c + ld boot/boot boot block is 380 bytes (max 510) + mk obj/kern/kernel.img athena% make qemu-nox Booting from Hard Disk... 6828 decimal is XXX octal! entering test_backtrace 5 entering test_backtrace 4 entering test_backtrace 3 entering test_backtrace 2 entering test_backtrace 1 entering test_backtrace 0 leaving test_backtrace 0 leaving test_backtrace 1 leaving test_backtrace 2 leaving test_backtrace 3 leaving test_backtrace 4 leaving test_backtrace 5 Welcome to the JOS kernel monitor! Type \u0026#39;help\u0026#39; for a list of commands. K\u0026gt; PC 的物理地址空间 PC 的物理地址空间是 hard-wired 的,如下所示:\n+------------------+ \u0026lt;- 0xFFFFFFFF (4GB) | 32-bit | | memory mapped | | devices | | | /\\/\\/\\/\\/\\/\\/\\/\\/\\/\\ /\\/\\/\\/\\/\\/\\/\\/\\/\\/\\ | | | Unused | | | +------------------+ \u0026lt;- depends on amount of RAM | | | | | Extended Memory | | | | | +------------------+ \u0026lt;- 0x00100000 (1MB) | BIOS ROM | +------------------+ \u0026lt;- 0x000F0000 (960KB) | 16-bit devices, | | expansion ROMs | +------------------+ \u0026lt;- 0x000C0000 (768KB) | VGA Display | +------------------+ \u0026lt;- 0x000A0000 (640KB) | | | Low Memory | | | +------------------+ \u0026lt;- 0x00000000 早期,基于 16 位 Intel 8088 处理器的电脑,最大只能寻址 1M 的物理内存。所以地址空间是从 0x00000000 到 0x000FFFFF(20-bit,2^20=1M),而不是 0xFFFFFFFF。\n标记为 Low Memory 的 640K 内存区域,是早期 PC 可以使用的 RAM;实际上,跟早期的 PC,只能配置 16KB、32KB、64KB 的 RAM。\n0x000A0000 到 0x000FFFFF 的 384K 内存区域是有硬件为视频显示和外部设备固件保留的。\n0x000F0000 到 0x000FFFFF 的 64K 内存是为 BIOS(Basic Input/Output System,基本输入输出系统)保留的,这个是非常重要的一部分内存区域。早期的 PC 中,BIOS 保存在 ROM(read-only memory)中,但是现在保存在闪存(译者注:其实也就是内存)中。BIOS 的任务是执行基础的系统初始化工作,例如:激活 video card、检查内存等。初始化检查完成以后,BIOS 会从磁盘、CD-ROM 等位置加载操作系统,然后将机器的控制权交给操作系统。\nIntel 最后使用 80286(支持 16M)和 80386(支持 4G)处理器突破了 1M 的内存限制,但是整体 PC 的架构还是保留了低 1M 物理地址空间的原始布局,以确保与现有软件的兼容。所以现代的 PC 中存在一个从 0x000A0000 到 0x00100000 的特殊内存区域,所以 RAM 也被区分为 low memory 和 extended memory,low memory 指的就是“特殊区域”的前 640K 内存,extended memory 指的就是其他内存 RAM 内存区域。此外,32-bit 物理内存空间的最顶部,首先就是物理 RAM,他由 BIOS 保留,专为 32-bit PCI 设备使用。\n最新的 x86 处理器已经突破了 4G RAM 内存,因此地址空间可以进一步扩展到 0xFFFFFFFF 以上,因此,和 16-bit 一样,64-bit 处理器也需要为 32-bit 处理器留出一部分“特殊区域”的 RAM,以此来兼容 32-bit 程序。\n 这里就解答了:为什么很多程序都需要编译成 32 位版本和 64 位版本?\n就是因为 32-bit 处理器和 64-bit 处理器的物理内存空间不一样。\n 在本课程中,为了简单,其实也是 JOS 本身的设计局限性,JOS 只使用 PC 物理内存的前 256M,所以我们假设所有的 PC 都只有 32 位物理地址空间。\nROM BIOS 本节将学习使用 QEMU 的调试工具来研究 IA-32 计算机如何启动\n 首先,打开两个终端,切换到 lab 目录下,终端 A 输入 make qemu-gdb (或者 make qemu-nox-gdb),此时启动了 QEMU,但是在处理器执行第一条指令之前停止了,此时在等待 GDB 的连接。在终端 B 中,执行 make gdb,将看到下面的输出:\nathena% make gdb GNU gdb (GDB) 6.8-debian Copyright (C) 2008 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later \u0026lt;http://gnu.org/licenses/gpl.html\u0026gt; This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type \u0026#34;show copying\u0026#34; and \u0026#34;show warranty\u0026#34; for details. This GDB was configured as \u0026#34;i486-linux-gnu\u0026#34;. + target remote localhost:26000 The target architecture is assumed to be i8086 [f000:fff0] 0xffff0:\tljmp $0xf000,$0xe05b 0x0000fff0 in ?? () + symbol-file obj/kern/kernel (gdb) lab 提供了一个 .gdbinit 文件,该文件设置 GDB 来调试早期的 16-bit 引导程序,并连接侦听 QEMU。(如果不起作用,则需要手动添加 add-auto-load-safe-path 到 .gdbinit 文件中)\n[f000:fff0] 0xffff0:\tljmp $0xf000,$0xe05b 这行是 GDB 对将被执行的指令的反汇编,从这行我们可以得到如下信息:\n PC 从 0x000ffff0 物理地址开始执行,该地址位于为 BIOS ROM 保留的 64K 地址的最顶端。 PC 开始执行 CS=0xf000:IP=0xfff0 位置的代码(不清楚寄存器的可以学习下这篇文章:操作系统基础知识概览) 要执行的第一条指令是 jmp 指令,它跳转到段地址为 CS=0xf000:IP=0xe05b 处 为什么 QEMU 是这样开始的?\n 因为 8088 处理器就是这样设计的。为什么要这样设计?因为不这样设计的话,机器通电以后,CPU 不知道该执行哪块的指令。所以 PC 中的 BIOS 是 hard-wired(硬连线) 到地址 0x000f0000-0x000fffff 的,所以机器在上电以后,BIOS 会先拿到机器的控制权(因为此时 RAM 中没有处理器可以执行的程序),执行初始化检查,检查完以后,将控制权再交给操作系统。\nQEMU 模拟器自带了 BIOS,该 BIOS 被放置在处理器模拟物理地址空间的位置上,当处理器被 reset,模拟的处理器则会进入 real mode(实模式),并且设置 CS=0xf000:IP=0xfff0,所以将从 CS:IP 代码段开始执行。\n 段地址 CS=0xf000:IP=0xfff0 如何转变为物理地址?\n 电脑刚通电启动,会进入 real mode,该模式下地址转换通过公式(physical address = 16 * segment + offset)进行。当 CS=0xf000:IP=0xfff0 时,物理地址为:16 * 0xf000 + 0xfff0 = 0xf0000 + 0xfff0 = 0xffff0(2 进制乘以 16 相当于左移四位,16 进制下相当于左移一位)。\n0xffff0 是 BIOS(0x100000)内存区域最顶部的 16 个字节。BIOS 做的第一件事就是将 jmp 跳转到 BIOS 中最顶部的位置开始执行指令。\n 当 BIOS 开始运行时,它会设置一个中断描述符表(interrupt descriptor table),并且初始化各种设备,例如 VGA 等,这时 QEMU 终端会打出 \u0026ldquo;Starting SeaBIOS\u0026rdquo; 信息。\n初始化 PCI 总线和 BIOS 必须的设备后,将会查找可引导设备,例如软盘、硬盘、CD_ROM 等。最后,BIOS 会引导加载程序(boot loader),并将控制权移交给它。\nPart 2: The Boot Loader PC 的软盘和硬盘被划分为多个 512 字节的区域,该 512 字节区域被称为扇区(sectors)。扇区是磁盘的最小传输单位:每一次读写必须是一个或多个扇区,并且在扇区边界对齐。如果一个磁盘是可引导的,那么它的第一个扇区必须是引导扇区(boot sector),因为这是引导程序代码所在的位置。当 BIOS 找到可引导的软盘或者硬盘时,他将位于第一个扇区的 512-byte 的引导扇区加载到物理地址为 0x7c00-0x7dff 的内存中,然后使用 jmp 指令将 CS:IP 设置为 0000:7c00,将控制权传递给引导加载程序。就像 加载 BIOS 的地址一样,这些地址看起来是任意地址,但是它们对于 PC 而言,是标准化的,也是固定的。\n在 PC 的发展过程中,从 CD-ROM 引导的能力出现得晚得多,因此 PC 架构师借此机会稍微重新思考了引导过程。因此,现代的 BIOS 从 CD-ROM 引导的方式更复杂(也更强大)。CD-ROMs 使用 2048 字节的扇区代替 512 字节,这样 BIOS 就可以在控制权移交之前,从磁盘加载更大的引导镜像到内存中。\n对于 6.828 实验来说,我们使用传统的硬盘引导机制,这意味着引导加载程序必须容纳在 512 字节以内。引导加载程序(boot loader)包含一个汇编源文件(boot/boot.S)和一个 C 源文件(boot/main.c),可以详细看下这两个文件。引导加载程序(boot loader)必须执行两个 main functions:\n 首先引导加载程序(boot loader)将处理器从实模式(real mode)切换到 32 位保护模式(32-bit protected mode),因为只有在 32 位保护模式(32-bit protected mode)下,软件才能访问处理器物理地址空间中 1M 以上的所有内存。保护模式(protected mode)在《PC 汇编语言》 PDF 版本的 1.2.7 和 1.2.8 节有详细的介绍。这个阶段,你只需要了解段地址到物理地址的转换在在保护模式(protected mode)下是不同的,转换偏移量(offset)是 32 位,而不是 16 位的。 其次,引导加载程序(boot loader)通过 x86 特殊的 I/O 指令直接访问 IDE 磁盘设备寄存器,来从磁盘读取内核(kernel)。如果想更好地理解此处的特定的 I/O 指令的含义,请参考“IDE硬盘驱动器控制器”部分。本课程中,不需要深入了解特殊设备编程的知识:编写设备驱动程序其实是操作系统开发的一个非常重要的部分,也是最无趣的部分之一。 理解完引导加载程序(boot loader)源码后,接下来看下 obj/boot/boot.asm 文件,这个文件是使用 GNU Makefile 编译引导加载程序(boot loader)生成的引导加载程序(boot loader)的反汇编。这个反汇编文件可以很容易的看到所有引导加载程序(boot loader)的代码在物理内存中的确切位置,并且在 GDB 调试时跟容易的跟踪发生的事情。\n可以使用 b 命令在 GDB 中设置地址断点,例如:命令 b *0x7c00 会在地址 0x7c00 处设置一个断点。到达断点后,你可以使用 c 和 si 命令继续执行(c 使 QEMU 继续执行,直到遇到下一个断点或者在 QEMU 终端中按下 Ctrl-c;si N 可以一次执行 N 个指令)。\n如果要检查内存中要执行的指令(除了 GDB 自动打印的下一个要执行的指令之外),可以使用 x/i 命令。此命令的语法为 x/Ni ADDR,其中 N 是要反汇编的连续指令数,ADDR 是要开始反汇编的内存地址。\n简单画了个图,如下所示:\n加载内核 接下来,将根据 boot/main.c 文件来详细学习引导加载程序(boot loader)的 C 语言部分。(需要温习下 C 语言编程指针部分内容)\n什么是 ELF 二进制文件?\n 当编译并链接一个 C 程序(比如 JOS 内核程序)时,编译器会将每个 C 源文件(.c)转换成一个 object 文件(.o),object 文件包含汇编语言指令,这些指令以硬件所期望的二进制格式编码。\n然后,链接器将所有编译的 object 文件组合成一个二进制镜像(binary image),例如:obj/kern/kernel,在这里他就是一个 ELF 格式的二进制文件,表示“可执行和可链接格式”\n 我们可以简单的认为,ELF 可执行文件带有加载信息的头,信息头后面紧跟的是多个程序段,每个程序段都是连续的一组代码或者数据,会在指定的地址加载到内存中。引导加载程序(boot loader)不会修改代码或数据,他将其加载到内存中并开始执行。\n一个 ELF 二进制文件以一个固定长度的 ELF 头(fixed-length ELF header)开始,然后是一个可变长度的程序头(variable-length program header),程序头列出了要加载的每个程序段。inc/elf.h 文件中定义了这些 ELF 头,我们需要关注的程序段如下所示:\n .text:程序的可执行指令 .rodata:只读数据。比如编译器生成的 ASCII 字符串常量。(不会限制硬件写入) .data:数据部分保存程序的初始化数据,例如使用初始化器声明的全局变量(int x = 5) 当链接器开始计算程序的内存分布时,它会为未初始化的全局变量(例如:int x)保留空间,该空间位于名叫 .bss 的段(section)中,它紧跟在 .data 段(section)之后。在 C 语言中,“未初始化”的全局变量默认为 0。因此,不需要将 .bss 的内容存储在 ELF 二进制文件中,只需要链接器记录.bss 段(section)的地址和大小即可。加载程序或者程序自身必须将 .bss 段(section)内容设置为 0。\n输入命令 objdump -h obj/boot/boot.out 来查看 ELF 二进制文件内部的结构:\n[root@centos ~/6.828/lab]# objdump -h obj/boot/boot.out obj/boot/boot.out: file format elf32-i386 Sections: Idx Name Size VMA LMA File off Algn 0 .text 0000017e 00007c00 00007c00 00000054 2**2 CONTENTS, ALLOC, LOAD, CODE 1 .eh_frame 000000cc 00007d80 00007d80 000001d4 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 2 .stab 000006d8 00000000 00000000 000002a0 2**2 CONTENTS, READONLY, DEBUGGING 3 .stabstr 000007df 00000000 00000000 00000978 2**0 CONTENTS, READONLY, DEBUGGING 4 .comment 00000011 00000000 00000000 00001157 2**0 CONTENTS, READONLY 可以看到,这里 dump 出来的内容比我们上面提到的要多,但是其他的段(section)在本课程中不需要过多关注,它们大多数是保存着调试信息,这些信息通常包含在程序的可执行文件中,但是不会由程序加载器加载到内存中。\n需要特别关注一下 .text 部分的 “VMA”(链接地址)和 “LMA”(加载地址):\n 加载地址(LMA)表示 section 内容应该加载到的内存地址 链接地址(VMA)表示 section 希望执行代码的内存地址 链接器(linker)以各种方式在二进制文件中编码链接地址,例如当代码需要一个全局变量的地址,如果一个二进制文件从一个没有被链接的地址开始执行,那么这个二进制文件通常是无法正常工作的。(其实,也可以生成一个位置独立(position-independent)的代码,这类代码不包含绝对地址(absolute addresses)。这种方式在现代的共享库(shared libraries)中广泛使用,但他有性能和复杂性成本,在 6.828 中不使用。)\n如上面 terminal 输出所示,一般情况下,链接地址和加载地址相同。\n引导加载程序(boot loader)根据 ELF 程序头(program headers)决定如何去加载 sections。程序头(program headers)标记了 ELF 对象的每个部分应该加载到内存中的哪个目标地址。输入命令 objdump -x obj/kern/kernel 可以查看程序头(program headers):\n[root@centos ~/6.828/lab]# objdump -x obj/kern/kernel obj/kern/kernel: file format elf32-i386 obj/kern/kernel architecture: i386, flags 0x00000112: EXEC_P, HAS_SYMS, D_PAGED start address 0x0010000c Program Header: LOAD off 0x00001000 vaddr 0xf0100000 paddr 0x00100000 align 2**12 filesz 0x0000ee71 memsz 0x0000ee71 flags r-x LOAD off 0x00010000 vaddr 0xf010f000 paddr 0x0010f000 align 2**12 filesz 0x0000a948 memsz 0x0000a948 flags rw- Sections: Idx Name Size VMA LMA File off Algn 0 .text 0000178e f0100000 00100000 00001000 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 .rodata 00000704 f01017a0 001017a0 000027a0 2**5 CONTENTS, ALLOC, LOAD, READONLY, DATA 2 .stab 000044d1 f0101ea4 00101ea4 00002ea4 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 3 .stabstr 00008afc f0106375 00106375 00007375 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 4 .data 0000a300 f010f000 0010f000 00010000 2**12 CONTENTS, ALLOC, LOAD, DATA 5 .bss 00000648 f0119300 00119300 0001a300 2**5 CONTENTS, ALLOC, LOAD, DATA 6 .comment 00000011 00000000 00000000 0001a948 2**0 CONTENTS, READONLY SYMBOL TABLE: f0100000 l d .text 00000000 .text f01017a0 l d .rodata 00000000 .rodata f0101ea4 l d .stab 00000000 .stab # 省略...... f0101ea4 g .stab 00000000 __STAB_BEGIN__ f01011e8 g F .text 00000016 strlen f0101315 g F .text 0000001d strchr f0100648 g F .text 000000cb mon_kerninfo f0100762 g F .text 00000136 monitor f0101459 g F .text 0000001b memfind f0100713 g F .text 00000045 mon_help 如上所示,可以在 “Program Header” 关键字下看到程序头(program headers)。需要被加载到内存中的 ELF 对象区域被 “LOAD” 关键字标记。程序头(program headers)中还可以看到其他信息,比如:虚拟地址 vaddr、物理地址 paddr、加载区域的大小 memsz \u0026amp; filesz。\n回看 boot/main.c 文件,每个程序头(program headers)的 ph-\u0026gt;p_pa 字段包含了段的目标物理地址。(注意:在本例中,它真的是一个物理地址,尽管 ELF 规范中对该字段没有明确说明。)\nBIOS 加载 boot sector 到内存中的起始地址为 0x7c00,因此该地址也是 boot sector 的加载地址。这个地址也是 boot sector 开始执行的位置,所以它也是一个链接地址。我们通过在 boot/Makefrag 文件中将 -Ttext 0x7c00 传递给链接器来设置链接地址。这样的话,链接器将在生成的代码中生成正确的内存地址。\n回顾下 kernel 的加载地址和链接地址,它们是不一样的,引导加载程序(boot loader)是一样的(00007c00)。kernel 告诉引导加载程序(boot loader)以较低的地址(1M 字节)将其加载到内存中,但是 kernel 想以更高的地址来执行代码。下一节中重点讨论这部分的实现。\n除了 section 信息之外,ELF 头中还有一个对我们很重要的字段,名为e_entry。该字段保存程序入口点(entry point)的链接地址:程序的 text 段(section)的内存地址,就是程序开始执行的位置。命令 objdump -f obj/kern/kernel 可以看到程序入口:\n[root@centos ~/6.828/lab]# objdump -f obj/kern/kernel obj/kern/kernel: file format elf32-i386 architecture: i386, flags 0x00000112: EXEC_P, HAS_SYMS, D_PAGED start address 0x0010000c 至此,我们应该了解了 boot/main.c 中的小型 ELF 加载器的大致功能:它会从磁盘读取 kernel 中的每个 section,然后加载到对应 section 加载地址所在的内存区域中,然后跳转到 kernel 程序入口(entry point)处开始执行。\nPart 3: The Kernel 本节详细学习 JOS 的内核。\n 虚拟内存 使用虚拟内存解决位置依赖问题\n 如上文所说,引导加载程序(boot loader)的链接地址和加载地址是一样的,但是 kernel 的两个地址相差很大(加载地址是低地址内存区域,链接地址是高地址内存区域)。链接 kernel 比引导加载程序(boot loader)更复杂,因此加载地址和链接地址位于 kern/kernel.ld 的最顶部。\n操作系统内核通常被链接和运行在一个非常高的虚拟地址(virtual address),比如 0xf0100000,以便把处理器的虚拟地址空间较低一部分区域留给用户进程使用。这么做的原因会在下一个 lab 中详细讨论。\n很多机器在地址 0xf0100000 处已经没有物理内存,因此我们不能指望在那个地址存储内核。所以,我们会使用处理器的内存管理硬件(processor's memory management hardware)将虚拟地址 0xf0100000(内核代码预期运行的链接地址)映射到物理地址 0x00100000(引导加载程序(boot loader)将内核加载到的物理内存地址)。尽管内核的虚拟地址足够高,可以为用户进程留下大量地址空间,但是它还是会被加载到 0x00100000 这个物理内存上(译者注:32 位预留的 RAM,最底下 640K 是 16 位预留的 RAM),这个位置刚好位于 BIOS ROM 的上面。这种方法要求 PC 至少有几兆的物理内存(大于 1M 内存,0x100000 内核代码才能正常运行), 1990 年以前的 PC 估计不行。\n下一个实验,我们会映射 PC 物理地址空间底部的 256M 空间,从物理地址 0x00000000-0x0fffffff(256M)到虚拟地址 0xf0000000-0xffffffff。现在我们就明白了,为什么 JOS 只能使用前 256M 的物理内存!\n现在我们只映射前 4M 物理内存,这些内存足够我们启动和运行。我们在 kern/entrypgdir.c 文件中手动修改代码,来静态初始化页目录(page directory)和页表(page table)。现在不用了解它的工作机制,只需要了解它的效果就行。在 kern/entry.S 文件设置 CR0_PG 标志之前,内存引用被视为物理地址(严格来说,是线性地址(linear addresses),但是 boot/boot.S 设置了一个线性地址到物理地址的映射)。一旦 CR0_PG 标志被设置,内存引用就是虚拟地址了,该虚拟地址是由虚拟内存硬件(virtual memory hardware)从物理地址转换而来。entry_pgdir 将虚拟地址(0xf0000000-0xf0400000)转换到物理地址(0x00000000-0x00400000),并将虚拟地址(0x00000000-0x00400000)转换为物理地址(0x00000000-0x00400000)。任何不在这两个范围内的虚拟地址都将导致硬件异常,因为我们还没有设置中断处理,这会导致 QEMU 退出并 dumo 机器状态信息。(如果没有使用 6.828 补丁版本的 QEMU,则将无休止地重启)\n格式化控制台输出 大多数人认为直接使用 printf() 函数就行了,可能还认为它是 C 语言的 “primitives”(原语),但是在操作系统内核中,我们必须自己实现所有 I/O。\n大家需要学习一下 kern/printf.c、lib/printfmt.c、kern/console.c 这三个文件,搞清楚他们之间的关联。后面自然就知道为什么 printfmt.c 文件要单独放在 lib 目录下?\n栈(Stack) 栈是先进后出的数据结构,内存中栈顶在低地址、栈底在高地址。可以想象成一个帽子🎩\n 本实验的最后,我们学习 C 语言在 x86 上使用栈的方式,并在此过程中,编写一个新的内核监控函数,用于打印栈的 backtrace:也就是从嵌套的 call 指令所保存的指令指针到当前执行指针的列表信息。\n# High Address +------------+ | | arg 2 | \\ +------------+ \u0026gt;- 前一个函数的栈帧(stack frame) | arg 1 | / +------------+ | | ret %eip | / +============+ | saved %ebp | \\ %ebp-\u0026gt; +------------+ | | | | | local | \\ | variables, | \u0026gt;- 当前函数的栈帧(stack frame) | etc. | / | | | | | | %esp-\u0026gt; +------------+ / # Low Address x86 栈指针(esp 寄存器,extended stack pointer)指向栈上当前正在使用的最低位置(栈顶位置指针)。在为栈保留的区域中,该位置下方的所有内容都是空的(free)。push 一个值入栈时,栈指针(esp)需要减小(也就是向下移动),然后将值写入到栈指针(esp)指向的内存区域。相反的,pop 一个值出栈时,会先读取栈指针(esp)指向的内存值,然后栈指针(esp)增加(向上移动)。在 32-bit 模式下,栈只能保存 32 位的值,esp 寄存器事中可以被 4 整除。各种 x86 指令都是“硬连线”(hard-wired)来使用栈指针寄存器(esp)的。\nebp 寄存器(栈基址寄存器,extended base pointer,指向系统栈最上面一个栈帧「最新执行的一个函数」的底部)主要通过软件约定来与栈关联在一起的。进入一个 C 函数时,函数的底层代码通常会将上一个函数的基指针 push 到栈中,然后在函数操作期间将当前 esp 值复制给 ebp(ebp 不是必须的,主要是为了方便操作局部变量)。如果程序中的所有函数都遵守此约定,那么在程序执行中的任何 point,都可以通过栈去追溯:通过保存的 ebp 指针的链路,确定函数通过怎样的调用嵌套顺序来到达这个 point。这个功能还是特别有用的,例如:当某个特定函数由于传递了错误参数导致 assert 失败或 panic,但是不确定是谁传递了错误参数时,栈的 backtrace 可以轻松找到有问题的函数。\n0xff 参考 MIT 6.828 参考资料\n",
"ref": "/blog/2021/mit/6.828-3/"
},{
"title": "【MIT 6.828】2. 实验环境准备",
"date": "",
"description": "做实验之前,对需要的 toolchain 进行安装",
"body": "0x00 简述 我的实验环境:\n CentOS 7 0x01 工具安装 整个实验环境需要两类工具:ToolChain、QEMU 模拟器\n ToolChain 工具链 ToolChain 包含汇编器、连接器、C 编译器和 debug 工具。\n一般的现代 Linux 系统都自带了 6.828 需要的 ToolChain,命令 objdump -i 和 gcc -m32 -print-libgcc-file-name 如果都能执行成功,说明 ToolChain 工具链已经就绪。\n我的 CentOS 自带了 ToolChain 工具链,所以没有安装,如果有问题或者需要自己安装 ToolChain 工具链,可以参考官方文档和B 站 UP 主的视频指导,推荐B 站 UP 主的视频指导,该视频讲解的很详细。\n 更新补充: 使用 linux 自带的工具链还是有些问题,需要到 JOS 中将 conf/env.mk 中的 GCCPREFIX= 注释掉,比较麻烦,估计坑也不少,建议按照官方文档安装。\n 本人打包好的工具链安装包:点击下载\n安装命令如下:\ntar xjf gmp-5.0.2.tar.bz2;cd gmp-5.0.2;./configure --prefix=/usr/local;make;make install;cd .. tar xjf mpfr-3.1.2.tar.bz2;cd mpfr-3.1.2;./configure --prefix=/usr/local;make;make install;cd .. tar xzf mpc-0.9.tar.gz;cd mpc-0.9;./configure --prefix=/usr/local;make;make install;cd .. tar xjf binutils-2.21.1.tar.bz2;cd binutils-2.21.1;./configure --prefix=/usr/local --target=i386-jos-elf --disable-werror;make;make install;cd .. i386-jos-elf-objdump -i tar xjf gcc-core-4.6.4.tar.bz2;cd gcc-4.6.4;mkdir build;cd build ../configure --prefix=/usr/local --target=i386-jos-elf --disable-werror --disable-libssp --disable-libmudflap --with-newlib --without-headers --enable-languages=c MAKEINFO=missing make all-gcc make install-gcc export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib # 可以加到 ~/.bashrc 文件中,避免每次手动导入 make all-target-libgcc make install-target-libgcc cd ../.. i386-jos-elf-gcc -v tar xjf gdb-7.3.1.tar.bz2;cd gdb-7.3.1;./configure --prefix=/usr/local --target=i386-jos-elf --program-prefix=i386-jos-elf- --disable-werror;make all;make install;cd .. git clone https://github.com/mit-pdos/6.828-qemu.git qemu ./configure --disable-kvm --disable-werror --prefix=/usr/local --target-list=\u0026#34;i386-softmmu x86_64-softmmu\u0026#34; make \u0026amp;\u0026amp; make install # 下载课程 jos 代码: git clone https://pdos.csail.mit.edu/6.828/2018/jos.git lab QEMU 模拟器 QEMU 建议安装 6.828 专用的 path 版本,仓库为 https://github.com/mit-pdos/6.828-qemu.git,可参考官方文档安装,安装时的简单报错可以根据提示很容易修复。\n0x02 Troubleshooting 安装 QEMU 时 pixman 报错\n 首选:yum install pixman-devel git submodule update --init pixman 把 pixman 代码下载下来即可 编译 QEMU 时出现 glib-2.22 gthread-2.0 is required to compile QEMU\n yum install glib2、yum install glib2-devel 0xff 参考 MIT 6.828 参考资料\n",
"ref": "/blog/2021/mit/6.828-2/"
},{
"title": "【MIT 6.828】1. 课程介绍",
"date": "",
"description": "对课程整体内容先简单介绍,后期逐个点展开来讲",
"body": "0x00 简述 6.828 主要讲操作系统基本原理,包括虚拟内存、内核、用户模式等。\n课程组成形式,如下所示:\n├── 讲座 │ ├── 讲解 xv6 操作系统 │ └── 讲解操作系统新兴的概念,这部分会学习很多研究论文 ├── 实验 │ ├── Lab 1:Booting │ ├── Lab 2:Memory management │ ├── Lab 3:User environments │ ├── Lab 4:Preemptive multitasking │ ├── Lab 5:File system, spawn, and shell │ └── Lab 6:Network driver └── 文档读物 0x01 术语 xv6:xv6 是一个类 Unix 的教学使用的操作系统,MIT 基于 Sixth Edition Unix (aka V6) 版本的重新实现,也是基于 x86 的,但是比 x86 更贴近于教学学习。 JOS:JOS 比 xv6 更早期一些,只支持单核,比 xv6 更适合学生来学习,6.828 就使用的 JOS 来教学的。这个命名是由于 Josh Cates 的杰出贡献,所以以 Josh Cates 的名称首字母加上 OS 命名的。 QEMU Emulator:模拟器,可真实模拟出你的硬件环境,用于调试操作系统等底层系统 GDB:GNU 项目的调试工具,用于查看一个程序内部活动 GCC:编译器工具集,用于将高级语言编译成二进制可执行程序 SPIM 模拟器:一个指令集的模拟器,可以模拟简单指令集,如 MIPS 指令集。QtSpim 是带有图形界面的版本。软件可以直接打开汇编代码。 0xff 参考 MIT 6.828 参考资料\n",
"ref": "/blog/2021/mit/6.828-1/"
},{
"title": "【MIT 6.828】0. 参考资料",
"date": "",
"description": "6.828 课程使用到的参考资料",
"body": "非官方 MIT 6.828-神级OS课程-要是早遇到,我还会是这种 five 系列 [MIT] 6.828 操作系统工程导读 官方 《PC 汇编语言》 PDF 版本 《6.828: PC hardware and x86》PPT 课件 xv6 操作系统介绍 ",
"ref": "/blog/2021/mit/6.828-0/"
},{
"title": "MIT:6.828-操作系统引擎",
"date": "",
"description": "MIT Operating System Engineering 课程",
"body": "0x00 原版视频 document.getElementById(\"video-bilibili\").style.height=document.getElementById(\"video-bilibili\").scrollWidth*0.5+\"px\"; 0x01 UP 主实操视频【推荐】 document.getElementById(\"video-bilibili\").style.height=document.getElementById(\"video-bilibili\").scrollWidth*0.5+\"px\"; 0xff 题外话 MIT 6.828 是由 PDOS 发布的操作系统相关的公开课程。\nPDOS 是什么呢?PDOS 全称是 MIT Parallel \u0026amp; Distributed Operating Systems Group,即麻省理工学院平行与分布式操作系统研究组,这个小组致力于研究操作系统相关的内容。PDOS 隶属于 MIT CSAIL。\nMIT CSAIL 又是什么呢?CSAIL 全称是 MIT Computer Science \u0026amp; Artificial Intelligence Lab,即麻省理工学院计算机科学与人工智能实验室,是计算机领域的先驱,创造了很多有价值的东西。下设很多小组,分别研究不同的方向。\n",
"ref": "/awesome/mit-6.828/"
},{
"title": "操作系统基础知识概览",
"date": "",
"description": "操作系统作为计算机科学的基础课程,需要多多了解学习",
"body": "0x00 计算机硬件结构 +------------------+ | ======CPU======= | +--------+---------+ | +------------+--------------+ +-----+ | | +------+ | AGP +--------+ MCH(Memory Controller Hub)+----+Memory| +--+--+ | | +------+ | +------------+--------------+ +------+-------+ | | | +------------+--------------+ +-----+ | Display | | | | | | | | ICH(I/O Controller Hub) +-----+ --\u0026gt; +--------------+ | | | PCI | +---+--------+-------+----+-+ | --\u0026gt; | | | | | | +----------+ | +----+---+ | +-+-----+ | --\u0026gt; +---+ USB +-+ | ATA | | |Network| | | | +-------+--+ +----+---+ | +-------+ | --\u0026gt; | | | | | | +---+---+ +----+---+ +------+--+ +--+--------+ | --\u0026gt; | Mouse | |Keyboard| |Hard Disk| | Flash BIOS| | | +-------+ +--------+ +---------+ +-----------+ +-----+ 计算机体系架构需要知道以下内容:\n CPU 负责指令执行与控制; MCH - Memory Controller Hub 芯片连接高速部件,如内存、显卡; Memory 内存通过地址总线直接与 CPU 相连,并受控于它,通过数据总线与其传输数据; ICH - I/O Controller Hub 芯片连接各种外部低速输入输出设备,如鼠标、键盘、硬盘、 PCI 外部互连总线扩展设备。 整个体系架构中,CPU 在最上面,也是速度最快的(主频基本都是 GHz 级别),其次是通过 MCH(内存控制芯片)连接的内存和显卡,MCH 又被称为北桥芯片(上北下南),再下面连接的是南桥芯片 ICH(IO 控制芯片),ICH 芯片与各种低速设备连接。\n对于 CPU 来说,所有连接的设备都属于 IO 设备,指令需要从内存中输入,处理得到的数据可以写入内存、硬盘,或者输出到显卡,以此来输出到显示器上。\n0x01 指令集 3086 是 16 位的 CPU,一共可以寻址 65536 字节(64k),但是其实当时设计的是寻址 1M,所以需要 20 位寻址空间,一个 16 位的寄存器不够,那就用两个,采用分段寻址方式实现 20 位寻址空间。段寄存器 CS(代码段)、DS(数据段)、ES(扩展段)、SS(栈段),通用寄存器 AX、BX、CX、DX,其他控制寄存器 IP(指令指针寄存器)、SP(栈顶指针寄存器)、BP(栈基址寄存器)、SI、DI、FLAG 30386 是 32 位的 CPU\n CPU 的指令集也很多,但是基本可以分为两类,它们差别主要体现在指令与数据处理上:\n CISC - Complex Instruction Set Computer 复杂指令集,具有大量的指令和数据寻址方式; RISC - Reduced Instruction Set Computer 精简指令集,仅处理寄存器中的数据; PC 结构与 CPU 架构对比,分为以下两种情况,基本属于一一对应关系:\n 冯·诺依曼体系结构,对应 CISC 指令系统,如 x86、x86-64、Atom,常用于台式机或服务器:\n 数据与指令都存储在同一存储区中,取指令与取数据利用同一数据总线。 被早期大多数计算机所采用。 ARM7 是冯诺依曼体系结构简单,但速度较慢,取指不能同时取数据。 哈佛体系结构,对应 RISC 指令系统,如嵌入式中流行的 ARM、PowerPC、MIPS、Sparc、Alpha,常用于工控、移动设备:\n 程序存储器与数据存储器分开。 提供了较大的存储器带宽,各自有自己的总线。 适合于数字信号处理。 大多数 DSP 都是哈佛结构。 ARM9 是哈佛结构,取指和取数在同一周期进行,提高速度,改进哈佛体系结构分成三个存储区:指令区、数据区、共用区。 0x02 汇编语言 CPU 只能执行二进制指令,早期人们编程就是手写二进制指令,二进制指令不方便人类记忆和使用,所以就诞生了汇编语言,使用英文字符表示二进制指令,英文字符和二进制指令是一一对应的。\n所以针对不同的 CPU,有不同的指令集,那么相对应的汇编语言指令名称也就不尽相同。\n0x03 寄存器 CPU 只负责运算,不存储数据,那么从哪里获取数据,答案就是寄存器(而不是内存,内存相对 CPU 来说太慢了)。寄存器不需要寻址步骤,按名称来区分,比如 x86 的 ESP 寄存器用来存储栈的地址,CPU 直接取 ESP 的值就能拿到数据,不需要寻址。\n我们平时说的 16 位、32 位、64 位 CPU,指的就是寄存器的大小。\n8086 如何确定下一条指令的地址? 使用代码段寄存器和指令指针寄存器:CS+IP\n例如:CS=0xF000,IP=0x390A\n那么下一条指令的执行地址就是:(CS\u0026laquo;4) + IP = 0xF390A\n0x04 内存模型 内存与 CPU 使用地址总线、数据总线、控制总线相连。\n8086 的 1M 内存空间如下所示:\n+------------------+ \u0026lt;- 0x00100000 (1MB) | BIOS ROM | +------------------+ \u0026lt;- 0x000F0000 (960KB) | 16-bit devices, | | expansion ROMs | +------------------+ \u0026lt;- 0x000C0000 (768KB) | VGA Display | +------------------+ \u0026lt;- 0x000A0000 (640KB) | | | Low Memory | | | +------------------+ \u0026lt;- 0x00000000 如果想在屏幕上显示字符串,直接在 VGA 的内存(0xA0000-0xC0000)内写入数据即可。\nBIOS 则是机器上电以后最先执行的代码,固定的内存地址为 0xF0000-0xFFFFF。为什么要固定,因为不固定,BIOS 系统找不到该执行哪块代码。BIOS 代码一般是机器厂商出厂时固化写死的。\n0xff 参考 文档 [MIT] 6.828 操作系统工程导读 工具 QEMU Emulator:模拟器,可真实模拟出你的硬件环境,用于调试操作系统等底层系统 GDB:GNU 项目的调试工具,用于查看一个程序内部活动 GCC:编译器工具集,用于将高级语言编译成二进制可执行程序 SPIM 模拟器:一个指令集的模拟器,可以模拟简单指令集,如 MIPS 指令集。QtSpim 是带有图形界面的版本。软件可以直接打开汇编代码。 ",
"ref": "/blog/2021/computer-basics/os-basic/"
},{
"title": "MIT:6.824-分布式系统",
"date": "",
"description": "MIT 分布式系统课程",
"body": "0x00 《Distributed Systems》 document.getElementById(\"video-bilibili\").style.height=document.getElementById(\"video-bilibili\").scrollWidth*0.5+\"px\"; ",
"ref": "/awesome/mit-6.824/"
},{
"title": "Golang 标准库:context",
"date": "",
"description": "context 包定义了 Context 类型,可以用来跨进程传输信息、帮助 API 传输数据,并且可以携带 deadlines 或者 cancellation 信号",
"body": "0x00 简介 Context 是以链式的方式来保存数据:context A 派生出了 context B,context B 派生出了 context C \u0026hellip;\u0026hellip;\n当一个 context 被 canceled,所有从它派生的 context 都会被取消:B 被取消,C 同样也会被废弃\n调用 WithCancel、WithDeadline、WithTimeout 这三个函数,入参是 parent context,出参是包含了 parent context 的 child context 和 CancelFunc函数。\n 调用 CancelFunc 函数会废弃 child context,并且会废弃 child context 的 context,同样会切断与 parent context 的关联,也会暂停所有相关的定时器。 如果没有调用 CancelFunc 函数,只能等到 parent context 被废弃或者定时器被触发。 go 的 vet 工具会检查所有的 context 链中 CancelFuncs 函数是否被调用。 0x01 最佳实践 不要将 context 对象保存在结构体类型中。我们应该显式的传递 context 给需要的函数,而且 context 必须是第一个参数,通常命名为 ctx 不要传递一个 nil 的 context,即使你不需要他。此时可以使用 context.TODO 代替 context 的 value 只建议保存进程和 APIs 的请求数据,不建议用来传递函数的可选参数 context 是协程安全的,多个 goroutine 可以操作相同的 context 0x02 常用函数 WithCancel func WithCancel(parent Context) (ctx Context, cancel CancelFunc)\n WithCancel 返回一个 parent context 的副本 child context,child context 同时携带了一个 Done channel,当返回的 CancelFunc 函数被调用时 context's Done channel 就会被关闭,此外如果 parent context's Done channel 被关闭,child context's Done channel 同样也会被关闭。\ncancel context 意味着程序会释放掉与该 context 相关的所有资源,所以建议在操作完成后立即 cancel context\n如下示例,使用 canceled context 可以防止 goroutine 泄漏,gen 函数得到 ctx.Done() 的信号以后会结束该函数。\npackage main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { // gen generates integers in a separate goroutine and \t// sends them to the returned channel. \t// The callers of gen need to cancel the context once \t// they are done consuming generated integers not to leak \t// the internal goroutine started by gen. \tgen := func(ctx context.Context) \u0026lt;-chan int { dst := make(chan int) n := 1 go func() { for { select { case \u0026lt;-ctx.Done(): return // returning not to leak the goroutine \tcase dst \u0026lt;- n: n++ } } }() return dst } ctx, cancel := context.WithCancel(context.Background()) defer cancel() // cancel when we are finished consuming integers for n := range gen(ctx) { fmt.Println(n) if n == 5 { break } } } WithDeadline func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)\n WithDeadline 同样返回一个 child context,不过限制了 child context 的截止时间不超过 d,如果 parent context 的时间早于 child,那么截止时间统一等于较早时间。当截止时间到达或者 CancelFunc 函数被调用又或者 parent context's Done channel 被关闭,都会导致 child context's Done channel 被关闭。\npackage main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) const shortDuration = 1 * time.Millisecond func main() { d := time.Now().Add(shortDuration) ctx, cancel := context.WithDeadline(context.Background(), d) // Even though ctx will be expired, it is good practice to call its \t// cancellation function in any case. Failure to do so may keep the \t// context and its parent alive longer than necessary. \tdefer cancel() select { case \u0026lt;-time.After(1 * time.Second): fmt.Println(\u0026#34;overslept\u0026#34;) case \u0026lt;-ctx.Done(): fmt.Println(ctx.Err()) } } WithTimeout func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)\n 相当于是 WithDeadline(parent, time.Now().Add(timeout))\nCancelFunc type CancelFunc func()\n CancelFunc 作用是取消一项任务,不需要等待这个任务执行完成。CancelFunc 可能被多个 goroutine 同时调用,第一次调用以后,后边的调用没有任何效果。\nContext 接口 type Context interface { Deadline() (deadline time.Time, ok bool) // 返回截止时间 Done() \u0026lt;-chan struct{} // 返回一个被关闭的 channel,如果 context 永远不会被 canceled,则返回 nil。 Err() error // 如果 Done 没有被关闭,返回 nil Value(key interface{}) interface{} // 返回对应 key 的 val } Background 方法 func Background() Context\n Background 方法返回一个非 nil 的空 Context 对象。这个 Context 永远不会被 canceled,而且没有 deadline。\n这个一般用在主函数、初始化函数等场景,作为最顶层 Context 存在。\nTODO 方法 func TODO() Context\n TODO 也返回一个非 nil 的空 Context 对象。使用场景:当前不清楚使用哪个 Context、当前 Context 参数不可用,以后再扩展。\nWithValue 方法 func WithValue(parent Context, key, val interface{}) Context\n WithValue 返回一个 parent context 的副本,这个副本包含了 key-val 键值对。key 必须是可比较的,为了避免冲突并且不应该是 string 或者其他内置类型,WithValue 的使用者建议自定义一种类型作为 key 来使用, 不能是 interface 类型,必须是 struct 类型。Alternatively, exported context key variables\u0026rsquo; static type should be a pointer or interface.\npackage main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; ) func main() { type favContextKey string f := func(ctx context.Context, k favContextKey) { if v := ctx.Value(k); v != nil { fmt.Println(\u0026#34;found value:\u0026#34;, v) return } fmt.Println(\u0026#34;key not found:\u0026#34;, k) } k := favContextKey(\u0026#34;language\u0026#34;) ctx := context.WithValue(context.Background(), k, \u0026#34;Go\u0026#34;) f(ctx, k) f(ctx, favContextKey(\u0026#34;color\u0026#34;)) } 参考:\n📌 context 官方文档\n",
"ref": "/blog/2020/go/go-standard-library-context/"
},{
"title": "设计模式 # 结构型 # 装饰器模式",
"date": "",
"description": "结构型设计模式之一,允许向一个现有对象添加新功能,同时又不改变其结构",
"body": " 结构型设计模式主要用于描述对象之间的组合,通过对象间的组合来完成特定功能。\n 0x00 模式概述 装饰器模式会动态的给一个对象添加新功能,就增加功能来说,装饰器模式比子类化更灵活(合成复用原则)。\n0x01 场景 当需要给一个对象增加新功能时,又不想使用继承,可以考虑使用装饰器模式实现。\n0x02 解决方案 装饰器模式通过将对象包装在装饰器类内部来动态更改对象的行为。\n0x03 总结 装饰器模式很简单,大多数脚本语言已经在语言层面实现了该模式。\n 参考:\n📌 设计模式(45种)\n📌 Design patterns for humans 中文版\n",
"ref": "/blog/2020/architecture/decorator-pattern/"
},{
"title": "设计模式 # 创建型 # 单例模式",
"date": "",
"description": "最简单的创建型设计模式之一,对外提供全局唯一的对象",
"body": "0x00 模式概述 单例模式是最简单的设计模式之一,属于创建型,提供了一种创建全局唯一对象的方法。\n0x01 场景 在业务代码中,如果需要全局唯一的对象,可以使用单例模式来创建对象。\n0x02 解决方案 单例模式根据对象实例化的时机,划分为懒汉式和饿汉式,顾名思义:\n 饿汉式:在类初始化的时候,就提前实例化一个对象,等到使用的时候直接返回 懒汉式:在第一次准备实例化对象的时候才真正创建一个对象,再此之后的实例化都是直接返回第一次创建的对象 饿汉式的优势是因为它是线程安全的,因为对象在使用之前已经创建了。而懒汉式就存在多线程并发情况下会创建出多个实例对象的问题,所以就需要加锁,加锁会影响性能。\n0x03 总结 一般情况下使用饿汉式就可以了,除非明确要求延迟初始化(lazy initialization)。\n 参考:\n📌 设计模式(45种)\n📌 Design patterns for humans 中文版\n",
"ref": "/blog/2020/architecture/singleton-pattern/"
},{
"title": "设计模式 # 创建型 # 原型模式",
"date": "",
"description": "创建型设计模式之一,适用于创建新对象成本比较高的场景,通过 clone 现有对象来创建新对象,以此降低成本",
"body": "0x00 模式概述 原型模式用于创建重复的对象,该重复对象的创建成本比较高,所以原型模式通过 clone 现有对象来完成。\n0x01 场景 适用于创建实例成本比较高的场景,eg:创建对象时需要先连接数据库,这个成本就很高,原型模式很适合。\n0x02 解决方案 // 克隆羊多利 class Sheep { protected $name; protected $category; public function __construct(string $name, string $category = \u0026#39;Mountain Sheep\u0026#39;) { $this-\u0026gt;name = $name; $this-\u0026gt;category = $category; } public function setName(string $name) { $this-\u0026gt;name = $name; } public function getName() { return $this-\u0026gt;name; } public function setCategory(string $category) { $this-\u0026gt;category = $category; } public function getCategory() { return $this-\u0026gt;category; } } // client $original = new Sheep(\u0026#39;Jolly\u0026#39;); echo $original-\u0026gt;getName(); // Jolly echo $original-\u0026gt;getCategory(); // Mountain Sheep // Clone and modify what is required $cloned = clone $original; $cloned-\u0026gt;setName(\u0026#39;Dolly\u0026#39;); echo $cloned-\u0026gt;getName(); // Dolly echo $cloned-\u0026gt;getCategory(); // Mountain sheep 0x03 总结 这个模式很简单,当创建的成本比较高时,就可以考虑使用 clone 的方式(原型模式)。\n 参考:\n📌 设计模式(45种)\n📌 Design patterns for humans 中文版\n",
"ref": "/blog/2020/architecture/prototype-pattern/"
},{
"title": "设计模式 # 创建型 # 构建器模式",
"date": "",
"description": "创建型设计模式之一,适用于创建对象由多个步骤组成的类型,防止构造函数无限扩大",
"body": "0x00 模式概述 构建器模式用来创建复杂的对象,该复杂对象由多个步骤构建而来。\n为了分离这种复杂性,新增一个对象 Builder 来一步一步创建最终的对象,这种创建实例的方式真的是非常优雅,后续如果进行需求变更,需要修改的内容就很少。\n0x01 场景 创建一个复杂的对象时,基础的对象不会变化,但是其组合会经常变化,此时使用构建器模式就非常有用。\n0x02 解决方案 // 复杂对象 class Burger { protected $size; protected $pepperoni = false; protected $lettuce = false; protected $tomato = false; public function __construct(BurgerBuilder $builder) { $this-\u0026gt;size = $builder-\u0026gt;size; $this-\u0026gt;pepperoni = $builder-\u0026gt;pepperoni; $this-\u0026gt;lettuce = $builder-\u0026gt;lettuce; $this-\u0026gt;tomato = $builder-\u0026gt;tomato; } } // 构造器类 class BurgerBuilder { public $size; public $pepperoni = false; public $lettuce = false; public $tomato = false; public function __construct(int $size) { $this-\u0026gt;size = $size; } public function addPepperoni() { $this-\u0026gt;pepperoni = true; return $this; } public function addLettuce() { $this-\u0026gt;lettuce = true; return $this; } public function addTomato() { $this-\u0026gt;tomato = true; return $this; } public function build(): Burger { return new Burger($this); } } // client $burger = (new BurgerBuilder(14)) -\u0026gt;addLettuce() -\u0026gt;addTomato() -\u0026gt;build(); 0x03 总结 这个模式很简单,简单理解就是适用于复杂对象的构造函数会经常变更的场景。\n 参考:\n📌 设计模式(45种)\n📌 Design patterns for humans 中文版\n",
"ref": "/blog/2020/architecture/builder-pattern/"
},{
"title": "基础算法:分治法",
"date": "",
"description": "常用的基础算法之一,用于将很难处理的大问题,拆解成小问题处理,然后合并小问题的解,得到大问题的解",
"body": "0x00 算法概述 分治法的思想就是将不可能或者很难解决的问题,拆解成多个相似的子问题,然后将子问题拆解成更小粒度的子问题,直到这些小问题可以很容易的被解决,然后合并这些小问题的解以得到最终的解。\n分治法是很多高效算法的基础,例如快排,归并排序等\n0x01 解题步骤 满足以下所有特征才能使用分治法:\n 该问题缩小到一定规模,就可以很容易的解决 具有最优子结构,即小规模问题和大规模问题是相同的问题(其实就是递归思想,可以使用递归来解决) 子问题的解可以合并成为大问题的解(这是关键特性,如果不满足,可以使用动态规划或贪心算法) 子问题之间是独立的(不独立的话也可以使用分治法,但是就是比较复杂,需要处理公共的子问题) 步骤:\n 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题; 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题 合并:将各个子问题的解合并为原问题的解。 0x02 实现方式 递归 参考:\n📌 五大常用算法之一:分治算法\n",
"ref": "/blog/2020/algorithm/divide-and-conquer-algorithm/"
},{
"title": "基础算法:动态规划",
"date": "",
"description": "常用的基础算法之一,思想类似于分治法,不同点是子问题之间存在依赖,用来处理多阶段决策类问题",
"body": "0x00 算法概述 动态规划的思想类似于分支法,也是将待处理的问题拆分成多个子问题,按顺序求解子问题,前一个子问题的解为后一个子问题的求解提供了有用的信息。\n0x01 解题步骤 满足以下所有特征才能使用动态规划:\n 最优子结构:问题的最优解所包含的子问题的解也是最优的(因为只有这样,状态转移到最后得出的解才是全局最优解) 无后效性:子问题的状态一旦确定,不会受到后面子问题的影响 有重叠子问题:子问题之间不是独立的,后面的子问题会多次用到前面子问题的解(这个不是必要条件,这个条件会使动态规划算法更具优势) 步骤:\n 动态规划一般用来处理多阶段决策类问题,一般由初始状态开始,通过中间阶段的决策,最后得到结束的状态。这样会实现一个决策序列:初始状态 → │决策1│ → │决策2│ →…→ │决策n│ → 结束状态\n 划分阶段:按照问题特征,把问题划分成多个阶段,阶段必须是有序的或者可排序的,否则无法求解 确定状态:一般情况下,各阶段状态就是各子问题的解,这样才能推出最终的解 状态转移方程:由之前一个或者多个状态得出当前状态的公式(这个一般是最难找的) 寻找终止条件:状态转移方程一般是递推式,需要找到终止条件来结束程序 0x02 实现方式 动态规划最难的就是找状态和状态转移方程,有时候状态找不对就很难找到状态转移方程。\n代码实现起来也不是很复杂。\n 参考:\n📌 五大常用算法之二:动态规划算法\n",
"ref": "/blog/2020/algorithm/dynamic-programming/"
},{
"title": "基础算法:贪心算法",
"date": "",
"description": "常用的基础算法之一,用于求最优解,但不一定能得到全局最优解",
"body": "0x00 算法概述 贪心算法是在求解问题的时候,总是选择当前最优的解,不考虑全局最优解。\n选择当前最优的解的方法称为贪心策略,贪心策略必须保证拆分的子问题必须是无后效性的(当前状态不会影响之前的状态)。\n0x01 解题步骤 分析问题,抽象成数学模型 将问题拆分成多个子问题(子问题必须是无后效性的) 求解每一个子问题,得到子问题的局部最优解 合并子问题的最优解,得出全局解 贪心算法的核心就是找贪心策略,然后证明贪心策略中子问题的最优解一定会得到全局最优解。\n0x02 实现方式 适用场景:\n 单源最短路经问题 最小生成树问题 可任意分割的背包问题。如果不可以任意分割,就需要用动态规划求解。 某些情况下,即使贪心算法不能得到整体最优解,但其最终结果近似于最优解。 贪心算法的实现很简单,只要能找到贪心策略,代码很容易写出来。\n 参考:\n📌 五大常用算法之三:贪心算法\n📌 常见算法及问题场景——贪心算法\n",
"ref": "/blog/2020/algorithm/greedy-algorithm/"
},{
"title": "基础算法:回溯法",
"date": "",
"description": "常用的基础算法之一,被称为通用解题方法",
"body": "0x00 算法概述 回溯算法其实是一种枚举算法(穷举法),是一种暴力解法,时间复杂度比较高。\n回溯法通过深度优先遍历的方法来尝试所有可能的解,当发现某一个分支无法满足求解条件,立马退回一步重新选择(回溯),尝试其他路径。如果只求一个解时,搜索到可用解后就停止搜索;如果求问题的所有解,就必须遍历所有解空间。\n0x01 解题步骤 针对所给问题,确定解空间:需要确定问题的解空间是否存在一个(最优)解 确定搜索规则 以深度优先策略开始搜索解空间,搜索过程中通过剪枝函数避免无效搜索 什么是剪枝函数?\n 剪枝函数是对无效解的过滤策略(明知道这条路径走下去不会得到解或最优解,所以就提前回溯,提高效率) 可行性剪枝:提前判断当前路径无法求出解,就可以提前回溯 最优化剪枝:声明一个变量存储当前最优解,如果可以提前判断当前路径无法满足最优解的条件,就提前回溯 剪枝函数特别难找,好的剪枝函数可以极大的降低算法的时间复杂度 回溯通常是通过反转动态规划的步骤来实现的 0x02 实现方式 非递归 递归 参考:\n📌 五大常用算法之四:回溯法\n📌 “通用解题法”之回溯中的“剪枝”\n",
"ref": "/blog/2020/algorithm/back-tracking/"
},{
"title": "AVL Tree 概述",
"date": "",
"description": "AVL 树是一种高度平衡的二叉搜索树",
"body": "0x00 前言 最早出现的是二叉树,随后人们发现二叉树可以用来二分查找,所以出现了二叉搜索树。\n但是二叉搜索树在极端情况会退化成单链表的形式,所以出现了平衡二叉搜索树,即AVL 树(AVL 取自发明它的两个人的名字首字母,话说老外都爱这个干,以自己名字命名各种算法)\n但是AVL 树也是有缺陷的,删除和插入效率很低(因为需要旋转多次),所以出现了红黑树,红黑树不是严格的平衡树,所以查找效率可能会低一点。\n本文主要介绍AVL 树,后续出单独的文章介绍红黑树。\n0x01 特性 必须是一颗二叉搜索树 每个节点的左子树和右子树的高度差的绝对值不能大于 1 查找、插入、删除的平均和最坏时间复杂度都是 O(logn) 0x02 术语 平衡因子(Balance Factor)\n 二叉树节点的左子树高度减去右子树高度的值,称为该节点的平衡因子 最小不平衡子树\n 距离插入节点最近的,且平衡因子绝对值大于 1 的节点为根的树,就是最小不平衡子树 0x03 实现 节点结构 下面的节点结构包含了节点的高度,也可以存储平衡因子和父节点。\nclass TreeNode(object): def __init__(self, value): self.value = value self.left = None self.right = None self.height = 0 AVL 类提供的函数 失衡调整 - 左单旋(在最小不平衡子树的右子树中插入右孩子时)\n+---+ +---+ +---+ | 4 | | 4 | | 5 | +---+ +---+ +---+ | | | | +---+ +---+ +---+ +---+ | 5 | | 5 | | 4 | | 6 | +---+ +---+ +---+ +---+ | +---+ | 6 | +---+ def leftRotation(proot): \u0026#34;\u0026#34;\u0026#34;单左旋转操作:param proot: 最小失衡子树的根节点:rtype: TreeNode\u0026#34;\u0026#34;\u0026#34; # 左旋 tmpNode = proot.right proot.right = tmpNode.left tmpNode.left = proot # 更新节点高度 proot.height = max(proot.left.height, proot.right.height) + 1 tmpNode.height = max(tmpNode.left.height, tmpNode.right.height) + 1 return tmpNode 失衡调整 - 右单旋(在最小不平衡子树的左子树中插入左孩子时)\n+---+ +---+ | 5 | | 5 | +---+ +---+ | | | | +---+ +---+ +---+ +---+ | 4 | | 6 | | 3 | | 6 | +---+ +---+ +---+ +---+ | | | +---+ +---+ +---+ | 3 | | 2 | | 4 | +---+ +---+ +---+ | +---+ | 2 | +---+ def rightRotation(proot): \u0026#34;\u0026#34;\u0026#34;单右旋转操作:param proot: 最小失衡子树的根节点:rtype: TreeNode\u0026#34;\u0026#34;\u0026#34; # 右旋 tmpNode = proot.left proot.left = tmpNode.right tmpNode.right = proot # 更新节点高度 proot.height = max(proot.left.height, proot.right.height) + 1 tmpNode.height = max(tmpNode.left.height, tmpNode.right.height) + 1 return tmpNode 失衡调整 - 右左旋(在最小不平衡子树的右子树中插入左孩子时)\n+----------+ +----------+ +----------+ | 5 | | 5 | | 5 | +----------+ +----------+ +----------+ | | | | | | +---+ +---+ +---+ +---+ +---+ +---+ | 3 | | 6 | | 3 | | 6 | | 3 | | 7 | +---+ +---+ +---+ +---+ +---+ +---+ | | | | | | | | | | +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ | 2 | | 4 | | 8 | | 2 | | 4 | | 7 | | 2 | | 4 | | 6 | | 8 | +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ | | +---+ +---+ | 7 | | 8 | +---+ +---+ def rightLeftRotation(proot): \u0026#34;\u0026#34;\u0026#34;右左旋转操作:param proot: 最小失衡子树的根节点:rtype: TreeNode\u0026#34;\u0026#34;\u0026#34; # 右旋,以 proot.right 为根进行旋转 proot.right = rightRotation(proot.right) # 左旋 return leftRotation(proot) 失衡调整 - 左右旋(在最小不平衡子树的左子树中插入右孩子时)\n+----------+ +----------+ +----------+ | 5 | | 5 | | 5 | +----------+ +----------+ +----------+ | | | | | | +---+ +---+ +---+ +---+ +---+ +---+ | 3 | | 7 | | 3 | | 7 | | 3 | | 7 | +---+ +---+ +---+ +---+ +---+ +---+ | | | | | | | | | | | | +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ | 2 | | 4 | | 6 | | 8 | | 2 | | 4 | | 6 | | 8 | | 1 | | 4 | | 6 | | 8 | +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ | | | | +---+ +---+ +---+ +---+ | 0 | | 1 | | 0 | | 2 | +---+ +---+ +---+ +---+ | | +---+ +---+ | 1 | | 0 | +---+ +---+ def leftRightRotation(proot): \u0026#34;\u0026#34;\u0026#34;左右旋转操作:param proot: 最小失衡子树的根节点:rtype: TreeNode\u0026#34;\u0026#34;\u0026#34; # 左旋,以 proot.left 为根进行旋转 proot.left = leftRotation(proot.left) # 右旋 return rightRotation(proot) 插入节点\ndef insert(pnode, key): \u0026#34;\u0026#34;\u0026#34;插入节点:递归来插入:param pnode: 插入的树:param key: 插入的值:return rootNode: 返回插入后的根节点:rtype: TreeNode\u0026#34;\u0026#34;\u0026#34; if pnode is None: # 递归终止条件 pnode = TreeNode(key) elif key \u0026gt; pnode.value: # 插入的值大于当前节点的值 pnode.right = insert(pnode.right, key) if abs(pnode.right.height - pnode.left.height) \u0026gt; 1: # 插入后失衡 if key \u0026gt; pnode.right.value: # 左单旋 pnode = leftRotation(pnode) elif key \u0026lt; pnode.right.value: # 右左旋 pnode = rightLeftRotation(pnode) elif key \u0026lt; pnode.value: # 插入的值小于当前节点的值 pnode.left = insert(pnode.left, key) if abs(pnode.left.height - pnode.right.height) \u0026gt; 1: # 插入导致失衡 if key \u0026lt; pnode.left.value: # 右单旋 pnode = rightRotation(pnode) elif key \u0026gt; pnode.left.value: pnode = leftRightRotation(pnode) pnode.height = max(pnode.left.height, pnode.right.height) + 1 return pnode 删除节点\n 删除节点也可能导致AVL树的失衡,实际上删除节点和插入节点是一种互逆的操作: 删除右子树的节点导致AVL树失衡时,相当于在左子树插入节点导致AVL树失衡。 删除左子树的节点导致AVL树失衡时,相当于在右子树插入节点导致AVL树失衡。 /*删除指定元素*/ template\u0026lt;typename T\u0026gt; AVLTreeNode\u0026lt;T\u0026gt;* AVLTree\u0026lt;T\u0026gt;::remove(AVLTreeNode\u0026lt;T\u0026gt;* \u0026amp; pnode, T key) { if (pnode != nullptr) { if (key == pnode-\u0026gt;key) //找到删除的节点 { //因AVL也是二叉排序树,删除节点要维护其二叉排序树的条件 if (pnode-\u0026gt;lchild != nullptr\u0026amp;\u0026amp;pnode-\u0026gt;rchild != nullptr) //若左右都不为空 { // 左子树比右子树高,在左子树上选择节点进行替换 if (height(pnode-\u0026gt;lchild) \u0026gt; height(pnode-\u0026gt;rchild)) { //使用左子树最大节点来代替被删节点,而删除该最大节点 AVLTreeNode\u0026lt;T\u0026gt;* ppre = maximum(pnode-\u0026gt;lchild); //左子树最大节点 pnode-\u0026gt;key = ppre-\u0026gt;key; //将最大节点的值覆盖当前结点 pnode-\u0026gt;lchild = remove(pnode-\u0026gt;lchild, ppre-\u0026gt;key); //递归地删除最大节点 } else //在右子树上选择节点进行替换 { //使用最小节点来代替被删节点,而删除该最小节点 AVLTreeNode\u0026lt;T\u0026gt;* psuc = minimum(pnode-\u0026gt;rchild); //右子树的最小节点 pnode-\u0026gt;key = psuc-\u0026gt;key; //将最小节点值覆盖当前结点 pnode-\u0026gt;rchild = remove(pnode-\u0026gt;rchild, psuc-\u0026gt;key); //递归地删除最小节点 } } else { AVLTreeNode\u0026lt;T\u0026gt; * ptemp = pnode; if (pnode-\u0026gt;lchild != nullptr) pnode = pnode-\u0026gt;lchild; else if (pnode-\u0026gt;rchild != nullptr) pnode = pnode-\u0026gt;rchild; delete ptemp; return nullptr; } } else if (key \u0026gt; pnode-\u0026gt;key)//要删除的节点比当前节点大,则在右子树进行删除 { pnode-\u0026gt;rchild = remove(pnode-\u0026gt;rchild, key); //删除右子树节点导致不平衡:相当于情况二或情况四 if (height(pnode-\u0026gt;lchild) - height(pnode-\u0026gt;rchild) == 2) { //相当于在左子树上插入右节点造成的失衡(情况四) if (height(pnode-\u0026gt;lchild-\u0026gt;rchild)\u0026gt;height(pnode-\u0026gt;lchild-\u0026gt;lchild)) pnode = leftRightRotation(pnode); else//相当于在左子树上插入左节点造成的失衡(情况二) pnode = rightRotation(pnode); } } else if (key \u0026lt; pnode-\u0026gt;key)//要删除的节点比当前节点小,则在左子树进行删除 { pnode-\u0026gt;lchild= remove(pnode-\u0026gt;lchild, key); //删除左子树节点导致不平衡:相当于情况三或情况一 if (height(pnode-\u0026gt;rchild) - height(pnode-\u0026gt;lchild) == 2) { //相当于在右子树上插入左节点造成的失衡(情况三) if (height(pnode-\u0026gt;rchild-\u0026gt;lchild)\u0026gt;height(pnode-\u0026gt;rchild-\u0026gt;rchild)) pnode = rightLeftRotation(pnode); else//相当于在右子树上插入右节点造成的失衡(情况一) pnode = leftRotation(pnode); } } return pnode; } return nullptr; }; 参考:\n📌 数据结构图文解析之:AVL树详解及C++模板实现\n",
"ref": "/blog/2020/algorithm/avl-tree/"
},{
"title": "LRU 算法概述",
"date": "",
"description": "LRU 算法是常用的缓存算法:最近最少被使用的缓存项将被淘汰掉",
"body": "0x00 算法概述 LRU(Least-recently-used):最近最少被使用的某个东西。最早使用在内存中,表示最近最少被使用的内存将会被释放。\n0x01 算法实现 如果想要这个算法的 get 和 set 时间复杂度都为 O(1),我们这里需要使用两种数据结构 dict 和双向链表。\ndict 获取元素的时间复杂度是 O(1),set 元素时通过字典先找到已存在的元素,然后将这个元素挪到链表的尾部,时间复杂度也是 O(1)。结构如下所示:\n+----------+ +----------+ +----------+ +----------+ | key-root | | key-A | | key-B | | key-C | +----------+ +----+-----+ +----+-----+ +----+-----+ | | | | | | | | v v v v +----+-----------------+----------------+------------------+----------+ | hash function | +----+-----------------+----------------+------------------+----------+ | | | | v v v v +----+-----+ +----+-----+ +---+------+ +----+-----+ +------\u0026gt;+ +-----\u0026gt;+ +------+ +-----\u0026gt;+ +--------+ | | root | | A | | B | | C | | | +----+ +\u0026lt;-----+ +------+ +\u0026lt;-----+ +\u0026lt;---+ | | | +----------+ +----------+ +----------+ +----------+ | | | | | | | +--------------------------------------------------------------------------+ | +---------------------------------------------------------------------------------+ 0x02 代码实现 在 Python 3 的内置模块 functools 中(Python 2 中没有),给出了 lru_cache 的实现,这应该就是用 Python 来实现 lru cache 的最佳实践了。\n有兴趣的可以看看源码,其实很简单,就是使用 0x01 中介绍的两种数据结构来实现的。\n另外 Python 中有一种数据类型是有序字典(OrderedDict,来自 collections 模块),也可以直接使用 OrderedDict 来实现 lru cache,其实 OrderedDict 的原理和上一种方式一样,只是将字典和双向链表的实现交给了 OrderedDict。\n 参考:\n📌 lru_cache 的实现\n",
"ref": "/blog/2020/algorithm/lru-cache/"
},{
"title": "Python 基础知识点",
"date": "",
"description": "总结 Python 中常用的基础知识点",
"body": " 以下知识点只给出结论,不给予论证,请自行测试。\n 0x00 实例属性的访问顺序 __getattribute__ 类的数据描述符属性 实例的属性 类的非数据描述符属性 类的普通属性 __getattr__ 0x01 实例属性的赋值顺序 类的数据描述符属性 实例属性 0x02 内存管理 引用计数 内存池 垃圾回收 0x03 垃圾回收 引用计数 标记清除 分代回收 0x04 作用域 global 关键字用来访问全局作用域的变量值。\nnonlocal 关键字(只有 Python 3 支持)用来访问 Enclosing locals(闭包)作用域的变量值。\nPython 没有块作用域,for 语句范围不是单独的作用域。\n Local Enclosing locals Global Built-in 0x05 is 关键字 is 用来比较变量的地址 == 用来比较变量的值 0x06 单引号/双引号/三引号 单引号和双引号等效,换行需要使用反斜杠(\\) 三引号可以直接换行,可以包含任何形式的字符串 0x07 自省 运行时能够查看对象内部的属性或状态 自省函数:type()、isinstance()、dir()、hasattr()、getattr() 0x08 staticmethod \u0026amp; classmethod staticmethod \u0026amp; classmethod 都是内置类型,属于非数据描述符 可以将类内的方法转变成静态方法和类方法(从 Python 2.2 以后才有) 0x09 单下划线 \u0026amp; 双下划线 __foo__:一种约定,Python 内部的名字,用来区别其他用户自定义的命名,以防冲突。 _foo:一种约定,用来指定变量私有,其实 Python 解析器没有真正的对其进行私有,只是用来提示程序员此变量是私有变量。 __foo:这个是间接的实现私有,解析器用_classname__foo来代替这个名字,以区别和其他类相同的命名。注意:继承的时候双下划线属性是不继承的,因为它会被重命名为_classname__foo。 0x10 AOP 编程 面向切面编程:在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面编程。 装饰器就是一种 AOP 编程思想 0x11 鸭子类型 鸭子类型是针对不同类型对象具有相同行为的一种统称。 参考:\n📌 InterV For Python\n",
"ref": "/blog/2020/python/python-basic-knowledge/"
},{
"title": "Python 标准库:abc",
"date": "",
"description": "在 Python 中用 abc 模块来创建抽象基类",
"body": "0x00 为什么需要 abc 模块 abc 模块为调用者和具体实现类(而不是抽象类)之间提供更高级别的语义化约定。你使用类 A 之前,类 A 就给你保证它有 b 方法和 c 属性,不需要你在使用的时候通过 getattr 来判断,这个就是约定(contract)。\n上面说的那句话是什么意思其实我也不是很懂,我理解它其实就类似于静态语言中的接口,子类必须实现抽象基类中的所有抽象方法和属性。而 abc 模块就帮你完成了这些事情。\n详情请查看 PEP 3119\n0x01 abc.ABCMeta 对象介绍 ABCMeta 是一个元类(metaclass),用来定义 Abstract Base Classes (ABCs)\nABCMeta 可以被继承,可以作为一个 Mixin Class。\n可以注册不相关的具体类和 ABCs,作为虚拟类,他和他的子类会被认为是注册的 ABC 的子类(使用内置的 issubclass 函数来判断),但是注册的 ABC 不会出现在他们的 MRO(Method Resolution Order)中,注册 ABC 的方法也不能被调用。\n通过 ABCMeta 元类创建的类具有以下方法:\n register(subclass)\n 注册一个子类,作为这个 ABC 的虚拟子类 注意:虚拟子类与 C++ 中的虚拟子类概念不是一回事 from abc import ABCMeta class MyABC: __metaclass__ = ABCMeta MyABC.register(tuple) assert issubclass(tuple, MyABC) assert isinstance((), MyABC) __subclasshook__(subclass)\n 必须是一个类方法。 检测 subclass 是不是这个 ABC 的子类。 这个方法被 __subclasscheck__ 调用,意思就是说可以通过这个类自定义 issubclass 的行为,不需要给每个需要注册虚拟子类的类调用 register() 方法(类似于 MyABC.register(tuple))。 这个方法必须返回 True,False 或者 NotImplemented。True 表示 subclass 是 ABC 的子类,False 反之,如果是 NotImplemented,那就继续执行接下来的常规流程。 给个例子解释下功能用法:\n ABC 类 MyIterable 定义了标准的可迭代对象方法(__iter__())作为抽象方法。这里给出的实现仍然可以在子类调用。get_iterator() 方法也是 MyIterable 的一部分,但是它不是抽象方法,所以它没有强制要求被非抽象子类重写(overridden)。 这里定义的 __subclasshook__ 方法表示,在传入的类型 C 的 MRO 中的所有类型的 __dict__ 中,如果有 __iter__ 方法,就返回 True(意思就是 C 是 MyIterable 的子类) 将 Foo 类注册为 MyIterable 的虚拟子类,这样的话,即使 Foo 没有定义 __iter__ 方法,它也会被认为是 MyIterable 的子类 class Foo(object): def __getitem__(self, index): ... def __len__(self): ... def get_iterator(self): return iter(self) class MyIterable: __metaclass__ = ABCMeta @abstractmethod def __iter__(self): while False: yield None def get_iterator(self): return self.__iter__() @classmethod def __subclasshook__(cls, C): if cls is MyIterable: if any(\u0026#34;__iter__\u0026#34; in B.__dict__ for B in C.__mro__): return True return NotImplemented MyIterable.register(Foo) 0x02 abc 模块装饰器介绍 abc.abstractmethod(function)\n 这个装饰器用来表示一个抽象方法 使用这个装饰器时,必须要求它所属的类的元类是 ABCMeta 或其子类 元类是 ABCMeta 或其子类的类,它的所有抽象方法和属性必须被重写(overridden),否则无法初始化 子类内可以通过 super() 方法来调用抽象方法的实现 动态给一个类添加抽象方法,或者修改一个方法为抽象方法都是不可以的 abc.abstractmethod 方法只能使用在正常继承方式下的子类,不能使用在虚拟子类(使用 register 方法注册的)中 注意:不像 JAVA 或者其他语言的抽象方法,Python 的抽象方法可以有自己的实现。这个实现可以被子类重写的方法通过 super() 方法调用,这个一般在多继承中很有用 abc.abstractproperty([fget[, fset[, fdel[, doc]]]])\n 内置 property 类的子类,表示一个抽象属性 使用这个装饰器时,必须要求它所属的类的元类是 ABCMeta 或其子类 元类是 ABCMeta 或其子类的类,它的所有抽象方法和属性必须被重写(overridden),否则无法初始化 子类内可以通过 super() 方法来调用抽象方法的实现 # 只读属性 class C: __metaclass__ = ABCMeta @abstractproperty def my_abstract_property(self): ... # 读写属性 class C: __metaclass__ = ABCMeta def getx(self): ... def setx(self, value): ... x = abstractproperty(getx, setx) 参考:\n📌 abc — Abstract Base Classes\n📌 PEP 3119\n📌 Why use Abstract Base Classes in Python?\n",
"ref": "/blog/2020/python/python-standard-library-abc/"
},{
"title": "设计模式 # 创建型 # 简单工厂、工厂方法、抽象工厂",
"date": "",
"description": "三种工厂类创建型设计模式的进化过程",
"body": "0x00 模式概述 这种三种设计模式都是属于创建型,都是为了将类的创建和使用隔离。GoF 23 种设计模式不包含简单工厂模式,因为简单工厂在扩展的时候需要修改源码,违反了开闭原则\n工厂方法模式是简单工厂的升级版,解决了扩展需要修改源码的问题,满足了开闭原则。\n抽象工厂模式是工厂方法的升级版,解决了工厂方法只能生产一类产品的问题,但是抽象工厂在增加新类型产品的时候会违反开闭原则,这里需要特别注意。\n0x01 场景 简单工厂 只有一个工厂生产产品,后期也不会扩展 工厂方法 多个工厂生产同一类产品,后期会扩展更多的工厂 抽象工厂 多个工厂生产多种类型的产品 0x02 解决方案 简单工厂 简单工厂的类图(不是标准的类图,只是凸显一下依赖关系)如下所示,client 需要使用产品类 A 时不直接实例化产品类 A,而是通过 SimpleFactory 工厂类的 createProd() 方法来创建产品类 A 实例。\n如果现在需要新增产品类 B,则需要修改 createProd() 方法的源码,这明显违反了开闭原则。\n+--------------------+ +---------------------------+ | | | SimpleFactory | | client +-------------\u0026gt;|---------------------------+ | | | +createProd(prodType) | +--------------------+ +---------------------------+ 工厂方法 所以出现了工厂方法,工厂方法其实是依赖倒置原则(面向接口编程,不要面向实现编程)的体现,client 不依赖具体的 SimpleFactory,而是依赖一个抽象的工厂 AbstractFactory,具体的工厂继承自 AbstractFactory,实现 createProd() 方法即可。\n增加新产品 C 时,只需要新增加 ConcreteFactoryC 类即可,满足开闭原则。\n+--------------------+ +---------------------------+ | | | AbstractFactory | | client +-------------\u0026gt;|---------------------------+ | | | +createProd() | +--------------------+ +----+----------------+-----+ ^ ^ | | | | +--------------+-----+ +----+---------------+ | ConcreteFactoryA | | ConcreteFactoryB | +--------------------+ +--------------------+ | +createProd() | | +createProd() | +--------------------+ +--------------------+ 抽象工厂 抽象工厂在工厂方法的基础上增加了新的产品类型(2 号产品),其他没有太大的区别。但是在扩展新产品类型(3号产品)的时候需要修改已有的代码(AbstractFactory、ConcreteFactoryA、ConcreteFactoryB 都需要增加 createProd3() 方法),破坏了开闭原则。\n+--------------------+ +---------------------------+ | | | AbstractFactory | | client +-------------\u0026gt;|---------------------------+ | | | +createProd1() | +--------------------+ | +createProd2() | +----+----------------+-----+ ^ ^ | | | | +--------------+-----+ +----+---------------+ | ConcreteFactoryA | | ConcreteFactoryB | +--------------------+ +--------------------+ | +createProd1() | | +createProd1() | | +createProd2() | | +createProd2() | +--------------------+ +--------------------+ 0x03 总结 简单工厂只适用于只有一个工厂生产一个产品的场景,扩展会破坏开闭原则 工厂方法适用于多个工厂生产同一类产品的场景,扩展时满足开闭原则 抽象工厂适用于多个工厂生产多种类产品的场景,扩展会破坏开闭原则 参考:\n📌 软件设计模式概述\n",
"ref": "/blog/2019/architecture/design-patterns-simple-factory-and-factory-method-and-abstract-factory/"
},{
"title": "设计模式概述",
"date": "",
"description": "简单介绍什么是设计模式,以及设计模式的 7 大原则",
"body": " 为什么需要设计模式?\n套用树人哥的一句话:其实世上本没有设计模式,走的人多了就有了设计模式。\n本来大家都是按照自己的方式写代码,然后根据不同需求,写出各种可扩展的、可维护的代码。然后,1995 年,GoF 四个人就将这些经验性代码总结出来,汇总成 23 种编程范式。\n其实就是编程的最佳实践,针对这种问题你就得这么干,因为根据历史经验来看,这么干是最优的。\n设计模式的本质是面向对象设计原则的实际应用。\n 0x00 设计模式分类 按目的来分 创建型:用于描述如何创建对象,将对象的创建与使用分离开 结构型:用于描述如何将类或对象按照某种布局组成更大的结构(类似于聚合) 行为型:用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务(类似于组合) 按作用范围来分 类模式:用于描述类与子类的关系,是静态的,在编译时就能确定下来(通过继承实现) 对象模式:用于描述对象之间的关系,在运行时刻是变化的(通过聚合/组合实现) 范围\\目的 创建型 结构型 行为型 类模式 工厂方法 适配器(类) 模板方法、解释器 对象模式 单例、原型、抽象工厂、建造者 代理、适配器(对象)、桥接、装饰、外观、享元、组合 策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录 0x01 Prin 1:开闭原则 Software entities should be open for extension, but closed for modification.\n 需求变更的时候,尽量通过扩展来实现需求,而不是通过修改源码来实现。 开闭原则是面向对象程序设计的终极目标 实现方法:通过抽象约束、封装变化来实现开闭原则,即通过接口或者抽象类为软件实体定义一个相对稳定的抽象层,而将相同的可变因素封装在具体实现类中。 0x02 Prin 2:里氏替换原则 Inheritance should ensure that any property proved about supertype objects also holds for subtype objects.\n 把父类都换成子类,代码的行为不会发生改变。 里氏替换原则是针对继承提出的原则:什么时候应该使用继承,什么时候不应该使用继承? 里氏替换原则是实现开闭原则的重要方式之一 克服了继承中重用父类造成的可用性变差的问题 实现方法:子类继承父类时,可以增加新的方法,不要重写父类的方法(如果重写了父类方法,在多态的情况下,不会出现期望的结果) 0x03 Prin 3:依赖倒置原则 High level modules shouldnot depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details. Details should depend upon abstractions.\n 核心思想:面向接口编程,不要面向实现编程 依赖倒置原则是实现开闭原则的重要方式之一,它降低了 Client 与具体实现之间的耦合 侧重点是层(类)与层(类)之间的解藕 实现方法:每个类尽量提供接口或者抽象类、任何类都不应该从具体类派生、继承时尽量遵循里氏替换原则 比如说 a 领导带领了 b 下属,这里 a 依赖于 b,此时又来了一个下属 c,然后 a 在分配任务的时候需要判断一下需要分给谁,如果再来一个下属 d,还需要再增加一个判断。这样显然很麻烦。这时我们抽象一个 a1,a 只把任务分配给 a1,即 a 依赖于 a1,b、c、d 继承 a1 实现具体的操作。这个就是依赖倒置了,倒置到需要依赖的类的抽象父类了。 依赖注入其实就是不在类内初始化对象(领导不直接询问下属的工作,而是通过 a1 提供给他),依赖注入是控制反转的实现方式,控制反转是依赖倒置的实现方式。 0x04 Prin 4:单一职责原则 There should never be more than one reason for a class to change.\n 核心思想:引起类变化的原因有且仅有一个,这一个原因就是表示单一职责 实现方法:接口划分的粒度问题,根据具体业务的不同而变化。没有最好,只有最合适。 0x05 Prin 5:接口隔离原则 Clients should not be forced to depend on methods they do not use.\n 关注点是接口依赖程度的隔离,Client 不应该被迫依赖于它不需要的方法 实现方法:缩小接口囊括的方法,接口设计里面没有银弹 0x06 Prin 6:迪米特法则 Talk only to your immediate friends and not to strangers.\n 核心思想:两个软件实体如果不需要直接交互,那么就不应该发生直接的相互调用,可以通过第三方转发调用。目的是降低类之间的耦合。 滥用迪米特法则会导致系统出现大量中间类,降低通信效率,所以凡事都有个度 0x07 Prin 7:合成复用原则 核心思想:在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系实现。 实现方法:一个对象通过包含一个已有对象成为一个新对象(聚合 or 组合) 参考:\n📌 软件设计模式概述\n📌 设计模式(45 种)\n",
"ref": "/blog/2019/architecture/design-patterns-overview/"
},{
"title": "MySQL 事务隔离级别",
"date": "",
"description": "MySQL 有四种事务隔离级别:RU、RC、RR(Default)、Serializable",
"body": " 说隔离级别之前,先说说并发会带来什么问题?\n 脏读:事务 A 读取了事务 B 更新的数据,然后 B 回滚了操作,那么 A 就是读取到了脏数据\n 不可重复读:事务 A 多次读取同一行记录,事务 B 在 A 多次读取过程中更新并提交了该记录,导致 A 多次读取的数据不一致(没有快照读)\n 幻读:事务 A 修改 id\u0026gt;10 的记录,使 name='cs\u0026rsquo;。但是事务 B 同时插入了 id=20 的一条记录,最后事务 A 发现 id=20 的记录没有被修改,仿佛出现了幻觉\n Note:不可重复读和幻读很容易混淆。不可重复读针对的是 update(更新某几条记录),解决不可重复度只需要锁住需要更新的某几条记录即可;幻读针对的是 insert/delete(增加/删除几条记录),解决幻读则需要给全表加锁。\n -- 查看 session 隔离级别: select @@tx_isolation; -- 修改 session 隔离级别: set session transacton isolation level xxxxx; -- 查看全局隔离级别: select @@global.tx_isolation; -- 修改全局隔离级别: set global transacton isolation level xxxxx; 0x00 Read-uncommitted RU 隔离级别下,上面三个问题都没有解决。\n从下面两个并发事务来看,事务 B 还没有 commit,事务 A 就能读到更新的数据,可见 RU 隔离级别是最低的,脏读都没有解决,不可重复读和幻读就更加不能解决了。\n+--------------------------------------------------------------------+------------------------------------------------------------------+ | Session A | Session B | +---------------------------------------------------------------------------------------------------------------------------------------+ | | | | mysql\u0026gt; set session transaction isolation level read uncommitted; | mysql\u0026gt; set session transaction isolation level read uncommitted; | | Query OK, 0 rows affected (0.00 sec) | Query OK, 0 rows affected (0.00 sec) | | | | | mysql\u0026gt; start transaction; | mysql\u0026gt; start transaction; | | Query OK, 0 rows affected (0.00 sec) | Query OK, 0 rows affected (0.00 sec) | | | | | mysql\u0026gt; select * from student; | mysql\u0026gt; select * from student; | | | | | +----+------+ | +----+------+ | | | id | name | | | id | name | | | +-----------+ | +-----------+ | | | 1 | cs | | | 1 | cs | | | +----+------+ | +----+------+ | | 1 row in set (0.00 sec) | 1 row in set (0.00 sec) | | | | | mysql\u0026gt; | mysql\u0026gt; update student set name=\u0026#39;yl\u0026#39; where id=1; | | | Query OK, 1 row affected (0.00 sec) | | mysql\u0026gt; | | | | mysql\u0026gt; select * from student; | | mysql\u0026gt; select * from student; | | | | +----+------+ | | +----+------+ | | id | name | | | | id | name | | +-----------+ | | +-----------+ | | 1 | yl | | | | 1 | yl | | +----+------+ | | +----+------+ | 1 rows in set (0.00 sec) | | 1 rows in set (0.00 sec) | | | | mysql\u0026gt; | | mysql\u0026gt; | | | | | +--------------------------------------------------------------------+------------------------------------------------------------------+ 0x01 Read-committed RC 隔离级别下解决脏读,但是没有解决不可重复读和幻读。\n从下面两个并发的事务来看,只有当事务 B 提交了事务以后,事务 A 才能读到数据,这也就是解决了脏读。\n但是,在同一个事务 A 内,出现了不可重复读的现象,连续读取同一条记录,数据不一致。\n+--------------------------------------------------------------------+------------------------------------------------------------------+ | Session A | Session B | +---------------------------------------------------------------------------------------------------------------------------------------+ | | | | mysql\u0026gt; set session transaction isolation level read committed; | mysql\u0026gt; set session transaction isolation level read committed | | Query OK, 0 rows affected (0.00 sec) | Query OK, 0 rows affected (0.00 sec) | | | | | mysql\u0026gt; start transaction; | mysql\u0026gt; start transaction; | | Query OK, 0 rows affected (0.00 sec) | Query OK, 0 rows affected (0.00 sec) | | | | | mysql\u0026gt; select * from student; | mysql\u0026gt; select * from student; | | | | | +----+------+ | +----+------+ | | | id | name | | | id | name | | | +-----------+ | +-----------+ | | | 1 | cs | | | 1 | cs | | | +----+------+ | +----+------+ | | 1 row in set (0.00 sec) | 1 row in set (0.00 sec) | | | | | | mysql\u0026gt; update student set name=\u0026#39;yl\u0026#39; where id=1; | | | Query OK, 1 row affected (0.00 sec) | | | | | mysql\u0026gt; select * from student; | mysql\u0026gt; select * from student; | | | | | +----+------+ | +----+------+ | | | id | name | | | id | name | | | +-----------+ | +-----------+ | | | 1 | cs | | | 1 | yl | | | +----+------+ | +----+------+ | | 1 row in set (0.00 sec) | 2 rows in set (0.00 sec) | | | | | mysql\u0026gt; select * from student; | mysql\u0026gt; commit; | | | Query OK, 0 row affected (0.00 sec) | | +----+------+ | | | | id | name | | mysql\u0026gt; | | +-----------+ | | | | 1 | yl | | | | +----+------+ | | | 2 rows in set (0.00 sec) | | | | | | mysql\u0026gt; | | | | | +--------------------------------------------------------------------+------------------------------------------------------------------+ 0x02 Repeatable-read RR 隔离级别下解决脏读和不可重复读,但是没有解决幻读。\n从下面两个并发的事务来看,即使事务 B 提交了事务以后,事务 A 还是不能读到数据,这就像开启事务 A 之前打了一个快照,读取到的数据还是开启事务之前的数据,所以也叫作快照读。\n但是,当事务 B 插入一条数据的时候,事务 A 虽然查不到数据,但是插入相同 id 的记录会报主键冲突,就跟产生了幻觉一样,这就叫幻读(只加了行锁,没有加全表锁导致的)。\n+--------------------------------------------------------------------+------------------------------------------------------------------+ | Session A | Session B | +---------------------------------------------------------------------------------------------------------------------------------------+ | | | | mysql\u0026gt; set session transaction isolation level repeatable read; | mysql\u0026gt; set session transaction isolation level repeatable read | | Query OK, 0 rows affected (0.00 sec) | Query OK, 0 rows affected (0.00 sec) | | | | | mysql\u0026gt; start transaction; | mysql\u0026gt; start transaction; | | Query OK, 0 rows affected (0.00 sec) | Query OK, 0 rows affected (0.00 sec) | | | | | mysql\u0026gt; select * from student; | mysql\u0026gt; select * from student; | | | | | +----+------+ | +----+------+ | | | id | name | | | id | name | | | +-----------+ | +-----------+ | | | 1 | cs | | | 1 | cs | | | +----+------+ | +----+------+ | | 1 row in set (0.00 sec) | 1 row in set (0.00 sec) | | | | | | mysql\u0026gt; update student set name=\u0026#39;yl\u0026#39; where id=1; | | | Query OK, 1 row affected (0.00 sec) | | | | | mysql\u0026gt; select * from student; | mysql\u0026gt; select * from student; | | | | | +----+------+ | +----+------+ | | | id | name | | | id | name | | | +-----------+ | +-----------+ | | | 1 | cs | | | 1 | yl | | | +----+------+ | +----+------+ | | 1 row in set (0.00 sec) | 1 rows in set (0.00 sec) | | | | | | mysql\u0026gt; commit; | | | Query OK, 0 row affected (0.00 sec) | | | | | mysql\u0026gt; select * from student; | mysql\u0026gt; | | | | | +----+------+ | | | | id | name | | | | +-----------+ | | | | 1 | cs | | | | +----+------+ | | | 1 rows in set (0.00 sec) | | | | | | mysql\u0026gt; | mysql\u0026gt; start transaction; | | | Query OK, 0 rows affected (0.00 sec) | | | | | | mysql\u0026gt; insert into student (name) values (\u0026#39;loVe\u0026#39;); | | | Query OK, 1 row affected (0.00 sec) | | | | | mysql\u0026gt; select * from student; | mysql\u0026gt; select * from student; | | | | | +----+------+ | +----+------+ | | | id | name | | | id | name | | | +-----------+ | +-----------+ | | | 1 | cs | | | 1 | cs | | | +----+------+ | | 5 | loVe | | | 1 rows in set (0.00 sec) | +----+------+ | | | 2 rows in set (0.00 sec) | | | | | | mysql\u0026gt; commit; | | | Query OK, 0 row affected (0.00 sec) | | mysql\u0026gt; insert into student (id,name) values (5,\u0026#39;like\u0026#39;); | | | ERROR 1062 (23000): Duplicate entry \u0026#39;5\u0026#39; for key \u0026#39;PRIMARY\u0026#39; | mysql\u0026gt; | | | | +--------------------------------------------------------------------+------------------------------------------------------------------+ 0x03 Serializable Serializable 隔离级别是最严格的级别,所有事务串行执行,即所有读写都加锁,虽然数据一致性的问题都解决了,但是这样效率很差,一般不使用。\n 参考:\n📌 MySQL的四种事务隔离级别\n",
"ref": "/blog/2019/db/mysql-transaction-isolation-level/"
},{
"title": "分布式事务:TCC",
"date": "",
"description": "2PC 是资源层面的分布式事务,TCC 是业务层面的分布式事务",
"body": " 什么是分布式?\n 与分布式对立的概念是单体系统,分布式系统是将不同的功能模块拆分成不同的服务,微服务就是一个典型的分布式系统。 分布式事务常见解决方案:\n 2PC两段提交协议 3PC三段提交协议(弥补两端提交协议缺点) TCC或者GTS(阿里) 消息中间件最终一致性 使用LCN解决分布式事物,理念“LCN并不生产事务,LCN只是本地事务的搬运工”。 0x00 TCC TCC 是 Try、Confire、Cancel 的缩写。\n又称补偿机制,核心思想是:针对每个操作(Try)都要注册一个与其对应的确认(Confirm)和补偿(Cancel)操作。\n0x01 5 个步骤 ①、向协调者发起开启事务请求\n②、Try 阶段:Try 阶段负责把所有服务的业务资源预留和锁住。类似于 MySQL DML 操作,会加行锁\n③、Confirm or Cancel 阶段:如果 Try 阶段涉及的所有服务都确认执行成功,则向协调者发送 Confirm(commit),否则发送 Cancel(rollback)\n④、协调者根据业务发送的 Confirm or Cancel,向所有服务发送相应的 Confirm or Cancel 请求\n⑤、各服务提交本地事务\n+----------------------+ 1 +------------------------+ | +---------------\u0026gt;+ | | APP | | Coordination service | | +---------------\u0026gt;+ | +-+--+--+------+--+--+-+ 3 +------------------------+ | | | | | | 2 | | | | | | 2 +------------------------+ | | | | +--------------------------+ | 4 | | | | 4 | | +------------------------+ | | +--------------------------+ | | | 4\u0026#39;| | 4\u0026#39; | | | | +----------------------+ +-------------------------+ | | | | | | | | v v v v v v +-+--+----+-------------------------+ +---------------------+---+--+-+ | service A | | service B | +----------------------+------------+ +-------+----------------------+ | | 5 | | 5 v v +-----+-------+ +---+---------+ | DB 1 | | DB 2 | +-------------+ +-------------+ 与 2PC 比较: 2PC 是资源层面的分布式事务,强一致,整个操作中资源一直被加锁,不需要开发者参与。\nTCC 是业务层面的分布式事务,需要开发人员自己来实现,最终一致性,不需要一直给资源加锁。补偿性事务。\nNote:\n TCC 不需要对资源加锁,所以性能比 2PC 好,但是需要额外开发 3 个接口(Try、Confirm、Cancel)。 Confirm 和 Cancel 可能会被多次调用,所以必须是幂等的。 参考:\n📌 分布式事务\u0026mdash;TCC原理\n",
"ref": "/blog/2019/architecture/distributed-transaction-tcc/"
},{
"title": "分布式事务:2PC & 3PC",
"date": "",
"description": "单体架构演化到微服务架构,分布式事务是必须要解决的问题",
"body": " 什么是分布式?\n 与分布式对立的概念是单体系统,分布式系统是将不同的功能模块拆分成不同的服务,微服务就是一个典型的分布式系统。 分布式事务常见解决方案:\n 2PC两段提交协议 3PC三段提交协议(弥补两端提交协议缺点) TCC或者GTS(阿里) 消息中间件最终一致性 使用LCN解决分布式事物,理念“LCN并不生产事务,LCN只是本地事务的搬运工”。 0x00 CAP 定理 所谓定理就是别人发现的规律,并且已经证实该规律是正确的(就像勾股定理,它永远是正确的,你直接用它的结论就行,你也可以自己证实其正确性)。\nCAP 定理定义:在异步网络中(不可靠的网络),不可能同时实现并发读/写状态下的 availability、consistency。即 Consistency、Availability 和 Partition Tolerance 不能同时满足。\n这里证明了 CAP 是成立的\n0x01 2PC Two-Phase Commit Protocol(两阶段提交协议),注意 ,2PC 是一个协议。\n2PC 是一个强一致、中心化的原子提交协议。这里面包含一个中心化的协调者节点(coordinator)。\nExample:订单服务 A,需要调用支付服务 B 去支付,支付成功则处理购物订单为待发货状态,否则就需要将购物订单处理为失败状态。\n两个阶段: Prepare 阶段\n①、事务询问:Coordinator 收到请求以后,向所有参与者发送事务预处理请求,即 Prepare。开始等待参与者响应\n②、执行本地事务:各参与者执行本地事务,但是不会真正提交,而是先向 Coordinator 报告自己的情况\n③、返回事务询问响应:参与者根据本地事务的情况返回 Yes or No\nCommit 阶段\n④、Coordinator 统计参与者的返回值,返送 commit(参与者全回复 Yes) 或者 rollback(有参与者回复 No)\n⑤、执行本地 commit 或者 rollbak\n| | request v +-----------------+-----------------+ | Coordinator | +-+------+----+-------+---+------+--+ | ^ | | ^ | | | | | | | 4 | 3 | | 1 1 | | 3 | 4 | | | | | | v | v v | v +-----+------+----+-+ ++---+------+-------+ | Server A | | Server B | +------+-----+------+ +---+-----+---------+ | | | | 5 | | 2 2 | | 5 | | | | v v v v +--+-----+---+ +-+-----+----+ | DB 1 | | DB 2 | +------------+ +------------+ 优缺点: 优点\n 原理简单(大多数关系型数据都是采用 2PC,eg:MySQL) 缺点\n 性能差:2PC 是一个阻塞协议,即节点在等待休息的时候会阻塞 单节点故障:Coordinator 作为一个中心化,如果单节点并且故障,整个分布式系统就会崩掉 数据不一致:Commit 阶段,由于网络问题只有部分参与者收到 Commit 命令,会导致数据不一致 0x02 3PC 3PC 的诞生主要是为了解决 2PC 出现的阻塞问题。与 2PC 相比提出了两个改动:\n Coordinator 和参与者都引入超时机制 在 2PC 的 Prepare 和 Commit 之前加入一个 check 阶段,以保证最后 Commit 阶段之前各参与节点状态一致 三个阶段: CanCommit 阶段\n①、锁询问:请求到达 Coordinator 后, Coordinator 发起 CanCommit 请求,各参与节点根据情况判断是否可以获取数据库锁\n②、锁请求:各参与节点尝试获取本地数据库锁\n③、锁返回:参与节点根据获取锁的情况返回 Yes or No\nPreCommit 阶段\n 如果所有参与节点都返回 Yes,则进入PreCommit 阶段,PreCommit 类似于 2PC 的Prepare 阶段,只是增加了超时机制,这里不再赘述 DoCommit 阶段\n DoCommit 阶段类似于 2PC 的 Commit 阶段 | | request v +------------------------+------------------------+ | Coordinator | +--+---+----+----+---+-------+---+----+----+---+--+ | ^ | ^ | | ^ | ^ | | | | | | | | | | | 7 | 6 | 4 | 3 | | 1 1 | | 3 | 4 | 6 | 7 | | | | | | | | | | v | v | v v | v | v +------+---+----+----+---+-+ +-+---+----+----+---+-----+ | Server A | | Server B | +---+---------+--------+---+ +---+---------+-------+---+ | | | | | | 8 | 5 | | 2 2 | | 5 | 8 | | | | | | v v v v v v +-+---------+--------+-+ +-+---------+-------+-+ | DB 1 | | DB 2 | +----------------------+ +---------------------+ 优缺点: 优点\n 在 2PC 的基础上,增加超时机制,缓解了阻塞的范围 能够在出现单点故障后继续达成一致 缺点\n 问题也是出现在优点 2 中,即使在出现单点故障的时候,参与者仍然会提交本地事务,这必然会出现数据不一致 参考:\n📌 分布式事务\u0026mdash;2PC和3PC原理\n📌 CAP Theorem\n📌 谈谈分布式系统的CAP理论\n📌 The Two-Phase Commit Protocol\n",
"ref": "/blog/2019/architecture/distributed-transaction-2pc-3pc/"
},{
"title": "视频:奥斯卡最佳动画短片 - Alike",
"date": "",
"description": "短片讲述如何不被世俗社会所安排,按自己的想法活着",
"body": "0x00 《Alike》 document.getElementById(\"video-bilibili\").style.height=document.getElementById(\"video-bilibili\").scrollWidth*0.5+\"px\"; ",
"ref": "/awesome/academy-award-for-best-animated-short-alike/"
},{
"title": "Python 标准库:socket",
"date": "",
"description": "socket 是比较底层的网络接口,所以必须掌握它的用法",
"body": " Btw:\nPython3 中,Queue 模块已经重命名为 queue。Python3.7 中增加了一个 SimpleQueue 类,其他内容同 Python2。\n 0x00 Overview Queue 模块实现的是多生产者、多消费者的队列。因此它是线程安全的,可以在多线程下使用。因为 Queue 类内部实现了所有必须的锁。\nQueue 模块提供了三种类型的队列,三种类型的主要差异是获取数据的顺序不同。\n0x01 队列 class:Queue(maxsize=0)\n FIFO(先进先出)队列。\n maxsize 用来设置队列的最大容量,一旦到达最大值,插入操作就会被阻塞住,直到队列内的内容被消费。\n 如果 maxsize 小于等于 0,队列的容量是无限大。\n class:LifoQueue(maxsize=0)\n LIFO(先进后出)队列,类似于栈。\n 其他规则同 Queue。\n class:PriorityQueue(maxsize=0)\n 优先队列,内部使用 heapq 实现。\n 其他规则同 Queue。\n 优先返回优先级低的数据,典型的数据模式是一个元组:(priority_number, data)\n exception:Empty\n 在一个空队列调用非阻塞 get() 或者 get_nowait() 时会抛出此异常。 exception:Full\n 在一个容量达到最大值的队列调用非阻塞 put() 或者 put_nowait() 时会抛出此异常。 0x02 Queue 对象介绍 LifoQueue,PriorityQueue 都是继承自 Queue\n Queue.qsize()\n 返回队列的近似大小,不能保证读写不会被阻塞。 Queue.empty()\n 判断队列是不是空,返回 True or False。不能保证读写不会被阻塞。 Queue.full()\n 判断队列是不是满的,返回 True or False。不能保证读写不会被阻塞。 Queue.put(item[, block[, timeout]])\n 将一个元素插入到队列中。如果 block=True 并且 timeout=None,如果队列满的话会阻塞,直到有空位置。\n 如果 timeout 是正数 x,它会阻塞 x 秒,这段时间内如果没有空位置,则会抛出一个 Full 异常。\n 如果 block=False,如果队列没有空位置,则会立即抛出一个 Full 异常。\n Queue.put_nowait(item)\n 等价于 put(item, False). Queue.get([block[, timeout]])\n 从队列移除并返回数据。如果 block=True 并且 timeout=None,如果队列为空的话会阻塞,直到有数据可用。\n 如果 timeout 是正数 x,它会阻塞 x 秒,这段时间内如果没有数据,则会抛出一个 Empty 异常。\n 如果 block=False,如果队列没有数据,则会立即抛出一个 Empty 异常。\n Queue.get_nowait()\n 等价于 get(False) Queue.task_done()\n 表示前一个排队的任务已经完成,常用于队列的消费线程。\n 调用 get() 获取一个任务以后,紧接着调用 task_done(),就是告诉队列:正在处理的任务已经完成。\n 如果阻塞在 join() 处,需要重新调用 task_done() 将所有任务处理完成。\n Queue.join()\n 调用以后就进行阻塞,直到所有的任务都被获取并处理完(task_done())。 一个消费线程调用 task_done(),未完成任务的数量就会减一。减到零以后就会停止阻塞。 参考:\n📌 Queue — A synchronized queue class\n",
"ref": "/blog/2019/python/python-standard-library-socket/"
},{
"title": "Leetcode:#32 最长有效括号",
"date": "",
"description": "Leetcode:#32 Longest Valid Parentheses,难度:困难",
"body": "0x00 题目描述 #32\n给定一个只包含 ( 和 ) 的字符串,找出最长的包含有效括号的子串的长度。\n输入: \u0026#34;(()\u0026#34; 输出: 2 解释: 最长有效括号子串为 \u0026#34;()\u0026#34; 输入: \u0026#34;)()())\u0026#34; 输出: 4 解释: 最长有效括号子串为 \u0026#34;()()\u0026#34; 0x01 约束 注意字符串为空的情况 0x02 题解 一般看到 最长、最优 等求最优解的题目,首先应该想到动态规划。\n 看到一个题,你首先应该能想到用什么现有的算法来解决,如果第一眼匹配不到现有的算法,那基本这个题你是解不出来的。因为不是专门研究算法的同学,能想到新的优秀的算法是很难的。\n 算法 1:无头绪 之前遇到一个类似的题,没有任何头绪。暴力破解肯定是能想出来的,但是是蠢办法。\n算法 2:动态规划 找状态和状态转移方程(类似于最长回文子串):\n 状态: dp[i][j] 表示从 i-j 的子串是不是有效的括号 第一次就将状态找错了,DP 也可以是一维的三维的,不要形成思维定式。这里就是一维的状态:dp[i] 中 i 表示在 s 中,下标以 i 结束的子串,有效括号的个数 状态转移方程: DP 最不好找的就是转移方程 这里分 3 种情况: 以 ( 结尾的肯定不是有效括号,直接忽略处理 以 ) 结尾的字符串中,如果 i-1 是 (,那么 dp[i] = dp[i-2] + 2(前两个字符索引处对应的最长括号有效个数 + 新增的 1 个有效括号) 以 ) 结尾的字符串中,如果 i-1 是 ),那么久追溯到 i-1 索引处对应的有效括号的最左端,即 i-dp[i-1],然后查看 i-dp[i-1]-1是不是 (,如果是的话,dp[i] = dp[i-1]+2+dp[i-dp[i-1]-2],其中 dp[i-dp[i-1]-2] 是前面的有效括号个数 def longestValidParentheses(s): sLen = len(s) if sLen \u0026lt; 2: return 0 dp = [0] * sLen for i in range(1, sLen): if s[i] == \u0026#39;)\u0026#39;: if s[i-1] == \u0026#39;(\u0026#39;: tmp = 0 if i-2 \u0026lt; 0 else dp[i-2] dp[i] = tmp + 2 else: if i-dp[i-1]-1 \u0026gt;=0 and s[i-dp[i-1]-1] == \u0026#39;(\u0026#39;: dp[i] = dp[i-1] + 2 + dp[i-dp[i-1]-2] return max(dp) PS:动态规划的时间复杂度其实也不小(O(n²)),只是避免了很多重复的计算。\n",
"ref": "/blog/2019/algorithm/leetcode-32-longest-valid-parentheses/"
},{
"title": "Leetcode:#10 正则表达式匹配",
"date": "",
"description": "Leetcode:#10 Regular Expression Matching,难度:困难",
"body": "0x00 题目描述 #10\n给定一个字符串 s 和一个字符规则 p,实现一个支持 . 和 * 的正则表达式匹配。其中 . 匹配任意单个字符,* 匹配零个或多个前面的那一个元素。\n所谓匹配,是要涵盖整个字符串 s,而不是 s 的子串。类似于 re.match 而不是 re.search。\n输入: s = \u0026#34;aa\u0026#34; p = \u0026#34;a\u0026#34; 输出: false 解释: \u0026#34;a\u0026#34; 无法匹配 \u0026#34;aa\u0026#34; 整个字符串。 0x01 约束 s 可能为空,且只包含 a-z 的小写字母 p 可能为空,且只包含 a-z 的小写字母,以及字符 . 和 * 0x02 题解 一般看到 最长、最优 等求最优解的题目,首先应该想到动态规划。\n 看到一个题,你首先应该能想到用什么现有的算法来解决,如果第一眼匹配不到现有的算法,那基本这个题你是解不出来的。因为不是专门研究算法的同学,能想到新的优秀的算法是很难的。\n 算法 1:无头绪 开始拿到这个题,我感觉无从下手,想不到什么好的办法(没有头绪)。\n只是想到 s 和 p 各拿一个指针,从左到右的扫描。接下来需要进行各种 if else 的判断,而且判断条件很复杂,不一定能做出来。\n算法 2:动态规划 开始没有想到这个题还可以使用 DP。\n那么什么样的问题可以使用 DP 呢?\n 关键点是:问题可以被拆分成子问题(要不然要状态干嘛!),子问题无后效性(也就是说子问题求解出来以后,后面的计算就不用关心前面的子问题了), 不要以为看到最优子结构这类问题才能用 DP。 DP 的核心是缩小解的空间,暴力破解是全局解,DP 排除了不可能成为最优解的项 什么是动态规划?- 徐凯强 Andy的回答 - 知乎 动态规划方法要寻找符合“最优子结构“的状态和状态转移方程的定义,在找到之后,这个问题就可以以“记忆化地求解递推式”的方法来解决。而寻找到的定义,才是动态规划的本质。 再回到这个问题上来。\n那就是对正则表达式问题进行重新定义,找到状态和状态转移方程(这个题的状态转移函数特别难找)。\n 状态:dp[i][j] 表示 s[0:i] 是否能匹配 p[0:j],状态值是 False 或者 True 转移方程:这个状态转移函数真的很难找,分为好几种情况,如下所示: 最简单的情况就是 s[i] == p[j] 或者 p[j] == '.',那就表示当前字符是匹配的,那就看 i-1 和 j-1 是否匹配,即 dp[i][j] == dp[i-1][j-1] 复杂的是当 p[j] == '*',需要分两种情况: 第一种情况是看 p[j] 的前一个字符(p[j-1])是否等于 . 或者与 s[j] 相等。此时需要判断三种情况,有一个满足要求即可: dp[i-1][j],这个表示 s[:i-1] 可以匹配 p[:j],说明此时的的 s[i] 是匹配了多个值(即 * 匹配了多个值) dp[i][j-1],表示 s[:i] 与 不带星号的 p[:j-1] 匹配上了,说明此时的 * 匹配了一个字母 dp[i][j-2],表示 s[:i] 与不带星号和星号前面的字符(p[:j-2])匹配上了,说明此时的 * 匹配了 0 个字母 剩下的就是第二种情况,p[j-1] 不等于 s[j],即表示此时的 * 表示的是 0 个字符,那么 dp[i][j] 需要参考 dp[i][j-2] 的匹配情况 class Solution: def isMatch(self, s: str, p: str) -\u0026gt; bool: sLen = len(s) pLen = len(p) if sLen == 0 and pLen == 0: return True dp = [[False] * (pLen + 1) for _ in range(sLen + 1)] dp[0][0] = True # only need to init empty `s` to all `p`, because dp init to False for i in range(1, pLen + 1): if p[i-1] == \u0026#39;*\u0026#39;: dp[0][i] = dp[0][i-2] for i in range(1, sLen + 1): for j in range(1, pLen + 1): if s[i-1] == p[j-1] or p[j-1] == \u0026#39;.\u0026#39;: dp[i][j] = dp[i-1][j-1] if p[j-1] == \u0026#39;*\u0026#39;: if s[i-1] == p[j-2] or p[j-2] == \u0026#39;.\u0026#39;: dp[i][j] = dp[i-1][j] or dp[i][j-1] or dp[i][j-2] else: dp[i][j] = dp[i][j-2] return dp[sLen][pLen] PS:动态规划的时间复杂度其实也不小(O(n²)),只是避免了很多重复的计算。\n 参考:\n📌 什么是动态规划(Dynamic Programming)?\n📌 状态转移函数参考这里\n",
"ref": "/blog/2019/algorithm/leetcode-10-regular-expression-matching/"
},{
"title": "Leetcode:#204 计数质数",
"date": "",
"description": "Leetcode:#204 Count Primes,难度:简单",
"body": "0x00 题目描述 #204\n统计所有小于非负整数 n 的质数的数量。\n0x01 约束 n 为非负整数\n 0 和 1 不是质数\n n 溢出的情况\n 0x02 题解 什么是质数? 质数就是只能被 1 和 自己整除的数字,也叫素数。\n算法 1:暴力破解法 暴力破解法是最简单也是最容易想到的算法\n 遍历 n 个数字,判断每个数字是不是质数,判断是不是质数的方法是遍历小于它的每个数字,看能不能被整除。 这个算法太愚蠢了,时间复杂度也很大 O(n²)。\n算法 2:埃拉托斯特尼筛法 我最初卡在如何判断一个数字是不是质数的问题上。其实判断一个数是不是质数,不需要判断比他小的每一个数字可不可以被整除,只需要判断到 [1-sqrt(n)]。\n比如说 12 = 2 x 6,12 = 3 x 4,12 = sqrt(12) x sqrt(12),12 = 4 x 3,12 = 6 x 2。从 sqrt(12) 分割,前半部分和后半部分是对称的,所以只需要判断 [1-sqrt(n)] 区间内的数字可不可以被整除就 ok 了。\n这样还是不够优化。\n这里使用 “埃拉托斯特尼筛法” 来优化。\n 算法核心是:排除法 把所有小于 sqrt(n) 的质数的倍数剔除,剩下的就是质数 那么为什么只需要剔除小于 sqrt(n) 的质数的倍数呢? 因为大于 sqrt(n) 的合数肯定能被因式分解成有限个质数的乘积,其中最小的质数肯定在小于 sqrt(n) 的范围内。所以只需要剔除小于 sqrt(n) 的质数的倍数,就可以完全覆盖大于 sqrt(n) 的合数。 def countPrimes(n): if n \u0026lt; 2: return 0 isPrime = [1] * n isPrime[0] = isPrime[1] = 0 # 0和1不是质数,先排除掉 # 埃式筛,把不大于根号n的所有质数的倍数剔除 for i in range(2, int(n ** 0.5) + 1): if isPrime[i]: isPrime[i * i:n:i] = [0] * ((n - 1 - i * i) // i + 1) return sum(isPrime) 参考:\n📌 Python最优解法\n",
"ref": "/blog/2019/algorithm/leetcode-204-count-primes/"
},{
"title": "Leetcode:#5 最长回文子串",
"date": "",
"description": "Leetcode:#5 Longest Palindromic Substring,难度:中等",
"body": "0x00 题目描述 #5\n给定一个字符串 s,找到 s 中最长的回文子串。\n输入: \u0026#34;babad\u0026#34; 输出: \u0026#34;bab\u0026#34; 注意: \u0026#34;aba\u0026#34; 也是一个有效答案。 0x01 约束 s 最大长度为 1000 注意字符串为空的情况 0x02 题解 一般看到 最长、最优 等求最优解的题目,首先应该想到动态规划。\n 看到一个题,你首先应该能想到用什么现有的算法来解决,如果第一眼匹配不到现有的算法,那基本这个题你是解不出来的。因为不是专门研究算法的同学,能想到新的优秀的算法是很难的。\n 算法 1:暴力破解法 这是一个蠢方法,把所有的子串找出来,判断每个子串是不是回文的,然后找出最长的那个子串返回。\n找出所有子串的时间复杂度是 O(n²)。\n算法 2:动态规划 找状态和状态转移方程:\n 状态:dp[i][j] 表示从 i-j 的子串是不是回文的 状态转移方程:dp[i][j] = s[i] == s[j] and (j-1 \u0026lt;= 2 or dp[i+1][j-1] == True) def longestPalindrome(s): sLen = len(s) if sLen == 0: return s dp = [[0] * sLen for _ in range(sLen)] ret = s[0] for j in range(1, sLen): # 如果使用 range(sLen) 来循环,提交会超时,可能与题目限制 s 最大为 1000 有关,多一次循环就给超时了 for i in range(j): if s[i] == s[j] and (j-i \u0026lt;= 2 or dp[i+1][j-1]): dp[i][j] = 1 if j-i+1 \u0026gt; len(ret): ret = s[i:j+1] return ret PS:动态规划的时间复杂度其实也不小(O(n²)),只是避免了很多重复的计算。\n",
"ref": "/blog/2019/algorithm/leetcode-5-longest-palindromic-substring/"
},{
"title": "Python 标准库:re",
"date": "",
"description": "Python 标准库 re(正则表达式)的用法总结",
"body": " NOTE:\n 正则表达式的 pattern 和被匹配的字符串可以是 Unicode 和 8-bit 字符串 正则表示式使用反斜杠(\\)来表示特殊的集合,要匹配反斜杠需要使用双反斜杠(\\)来表示。Python 中的字符串恰好也使用反斜杠(\\)来表示特殊字符或集合,所以要是用 Python 字符串来表示正则表达式中的反斜杠需要使用 4 个反斜杠(\\\\)。有一个简单的方法,就是使用 Python 的 raw string(字符串前加前缀 r),raw string 内的字符串不进行任何转译。 0x00 正则表达式语法 这个知识点很复杂,需要单独来讨论,先给个参考链接:Regular Expression Syntax\n0x01 模块内容 这里主要讲 re 模块内的函数使用\n re.compile(pattern, flags=0)\n 将正则表达式的 pattern 编译成 _sre.SRE_Pattern 对象。 通过指定 flag 的值可以改变正则表达式匹配的行为,flag 的值可以通过 | 操作符组合多个值。 re.compile() 返回的对象可以多次使用,不需要使用时再次编译,提高效率。但是,如何一个程序中只使用很少的几个正则表达式,那就不需要担心编译的问题了,因为 re.match()、re.search()、re.compile() 等都会将编译后的对象进行缓存(使用的正则表达式多了就不行了,因为毕竟缓存是有限的)。 re.DEBUG\n 显示被编译的表达式的 debug 信息 re.I \u0026amp;\u0026amp; re.IGNORECASE\n re.I 是 re.IGNORECASE 的缩写。 进行匹配的时候忽略大小写,如果需要对 Unicode 类型的字符使用这个效果,可以添加 re.UNICODE 标识 re.L \u0026amp;\u0026amp; re.LOCALE\n 使正则表达式中的 \\w、\\W、\\b、\\B、\\s、\\S 依赖于当前所在的区域。 re.M \u0026amp;\u0026amp; re.MULTILINE\n 指定正则匹配时进行多行匹配:^ 字符匹配字符串和**每一行开始(也就是换行符之后)的位置(默认情况下,^ 只匹配字符串开始的位置),$ 字符匹配字符串和每一行结尾(也就是换行符之前)**的位置(默认情况下,$ 只匹配字符串结尾,如果结尾是换行符就匹配到换行符之前的位置) re.S \u0026amp;\u0026amp; re.DOTALL\n 这个标识使正则表达式中的 . 字符可以匹配任何字符(包括换行符,默认不匹配换行符) re.U \u0026amp;\u0026amp; re.UNICODE\n 使 \\w、\\W、\\b、\\B、\\d、\\D、\\s、\\S 依赖于 Unicode 字符集,可以为 re.IGNORECASE 开启 non-ASCII 的匹配 re.X \u0026amp;\u0026amp; re.VERBOSE\n 这个标识允许你给正则表达式的 pattern 添加注释,注释使用 # 关键字,从最左边的 # 字符开始到行尾都是注释(前提是 # 不在 character class 中,或者前边没有转义反斜杠)。pattern 中的空白字符是被忽略的,除非在 character class 中,或者前边是一个非转义的反斜杠,或者在 *?、(?:、(?P\u0026lt;...\u0026gt; 中。 # 这两个正则表达式等价 a = re.compile(r\u0026#34;\u0026#34;\u0026#34;\\d + # the integral part\\. # the decimal point\\d * # some fractional digits\u0026#34;\u0026#34;\u0026#34;, re.X) b = re.compile(r\u0026#34;\\d+\\.\\d*\u0026#34;) re.search(pattern, string, flags=0)\n 扫描字符串(string),寻找第一个正则表达式匹配到的位置,返回一个 MatchObject 对象。如果在字符串中没有位置匹配,就返回 None。注意,this is different from finding a zero-length match at some point in the string(没太明白这句话什么意思) a=re.search(\u0026#34;a?bb\u0026#34;, \u0026#34;bbcc\u0026#34;) a.group() # \u0026#39;bb\u0026#39; a=re.search(\u0026#34;a?bb\u0026#34;, \u0026#34;bccabbcbb\u0026#34;) a.group() # \u0026#39;abb\u0026#39; re.match(pattern, string, flags=0)\n 其他的内容与 re.search 相同,不同的是, re.match 必须从字符串的开头开始匹配(re.search 可以从中间才开始匹配正则表达式)。 还有一点需要注意的是,即使在 re.MULTILINE 模式下,re.match 也只能在字符串的开头匹配(而不是每行的开头) re.split(pattern, string, maxsplit=0, flags=0)\n 从 pattern 匹配到的位置来分割字符串。 如果发现在 pattern 中有圆括号,会同时将所有的 groups 添加到列表中返回。 如果 maxsplit 非 0,则匹配到第 maxsplit 个就停止,将剩余的字符串追加到列表后面,然后返回。 如果在字符串的开始捕获到正则表达式中的组(groups,圆括号中的内容),则返回的列表第一个元素是空字符串,对于字符串的末尾也是一样的。 re.split(\u0026#39;\\W+\u0026#39;, \u0026#39;Words, words, words.\u0026#39;) # [\u0026#39;Words\u0026#39;, \u0026#39;words\u0026#39;, \u0026#39;words\u0026#39;, \u0026#39;\u0026#39;] re.split(\u0026#39;(\\W+)\u0026#39;, \u0026#39;Words, words, words.\u0026#39;) # [\u0026#39;Words\u0026#39;, \u0026#39;, \u0026#39;, \u0026#39;words\u0026#39;, \u0026#39;, \u0026#39;, \u0026#39;words\u0026#39;, \u0026#39;.\u0026#39;, \u0026#39;\u0026#39;] re.split(\u0026#39;\\W+\u0026#39;, \u0026#39;Words, words, words.\u0026#39;, 1) # [\u0026#39;Words\u0026#39;, \u0026#39;words, words.\u0026#39;] re.split(\u0026#39;[a-f]+\u0026#39;, \u0026#39;0a3B9\u0026#39;, flags=re.IGNORECASE) # [\u0026#39;0\u0026#39;, \u0026#39;3\u0026#39;, \u0026#39;9\u0026#39;] re.split(\u0026#39;(\\W+)\u0026#39;, \u0026#39;...words, words...\u0026#39;) # [\u0026#39;\u0026#39;, \u0026#39;...\u0026#39;, \u0026#39;words\u0026#39;, \u0026#39;, \u0026#39;, \u0026#39;words\u0026#39;, \u0026#39;...\u0026#39;, \u0026#39;\u0026#39;] re.findall(pattern, string, flags=0)\n 以列表的形式,返回所有不重叠的匹配。从左到右扫描字符串,匹配到的字符串按先后顺序返回。 注意,如果 pattern 中包含组(groups,圆括号中的内容),则返回组列表;如果有多个组,则返回的列表的每一项都是一个元组 re.findall(\u0026#34;a?bc\u0026#34;, \u0026#34;bcddabcessbbc\u0026#34;) # [\u0026#39;bc\u0026#39;, \u0026#39;abc\u0026#39;, \u0026#39;bc\u0026#39;] re.findall(\u0026#34;a?(bc)\u0026#34;, \u0026#34;bcddabcessbbc\u0026#34;) # [\u0026#39;bc\u0026#39;, \u0026#39;bc\u0026#39;, \u0026#39;bc\u0026#39;] re.findall(\u0026#34;(a?)(bc)\u0026#34;, \u0026#34;bcddabcessbbc\u0026#34;) # [(\u0026#39;\u0026#39;, \u0026#39;bc\u0026#39;), (\u0026#39;a\u0026#39;, \u0026#39;bc\u0026#39;), (\u0026#39;\u0026#39;, \u0026#39;bc\u0026#39;)] re.finditer(pattern, string, flags=0)\n 返回一个迭代器,这个迭代器会生成 MatchObject 对象,不会重叠匹配。 re.finditer 其实是 re.findall 的迭代器版本(类似于 range 和 xrange) re.sub(pattern, repl, string, count=0, flags=0)\n 用 repl 替换最左边的非重叠的匹配项,然后返回该字符串。如果没有匹配到,就返回原字符串。 repl 可以是字符串或者函数。 如果是字符串,任何 backslash escapes 都会被处理(\\n 会被认为是换行,不认识的转义会忽略)。特殊情况,\\1 会拿匹配到的第一个组来替换到 \\1 位置(叫做 backreferences)。\\g\u0026lt;2\u0026gt; 等同于 \\2,\\g\u0026lt;name\u0026gt; 使用的是命名的组 (?P\u0026lt;name\u0026gt;...),\\g\u0026lt;0\u0026gt; 使用的是整个字符串。 如果是函数,每次非重复匹配都会调用该函数,函数参数是匹配到的 MatchObject 对象,但会一个替换的字符串。 count 表示要替换的最大次数,必须是非负值,如果是 0,所有的匹配项都被替换。 Empty matches for the pattern are replaced only when not adjacent to a previous match, so sub(\u0026lsquo;x*', \u0026lsquo;-', \u0026lsquo;abc\u0026rsquo;) returns \u0026lsquo;-a-b-c-'. # repl is string re.sub(r\u0026#39;def\\s+([a-zA-Z_][a-zA-Z_0-9]*)\\s*\\(\\s*\\):\u0026#39;, r\u0026#39;static PyObject*\\npy_\\1(void)\\n{\u0026#39;, \u0026#39;def myfunc():\u0026#39;) #\u0026#39;static PyObject*\\npy_myfunc(void)\\n{\u0026#39; # repl is function def dashrepl(matchobj): if matchobj.group(0) == \u0026#39;-\u0026#39;: return \u0026#39;\u0026#39; else: return \u0026#39;-\u0026#39; re.sub(\u0026#39;-{1,2}\u0026#39;, dashrepl, \u0026#39;pro----gram-files\u0026#39;) #\u0026#39;pro--gram files\u0026#39; re.sub(r\u0026#39;\\sAND\\s\u0026#39;, \u0026#39;\u0026amp; \u0026#39;, \u0026#39;Baked Beans And Spam\u0026#39;, flags=re.IGNORECASE) #\u0026#39;Baked Beans \u0026amp; Spam\u0026#39; re.subn(pattern, repl, string, count=0, flags=0)\n 与 re.sub 操作相同,只不过返回的是元祖 (new_string, number_of_subs_made),新产生的字符串和替换的次数。 re.escape(pattern)\n 转义所有字符(除了 ASCII 字符和数字)。 print re.escape(\u0026#39;python.exe\u0026#39;) # python\\.exe legal_chars = string.ascii_lowercase + string.digits + \u0026#34;!#$%\u0026amp;\u0026#39;*+-.^_`|~:\u0026#34; print \u0026#39;[%s]+\u0026#39; % re.escape(legal_chars) # [abcdefghijklmnopqrstuvwxyz0123456789\\!\\#\\$\\%\\\u0026amp;\\\u0026#39;\\*\\+\\-\\.\\^\\_\\`\\|\\~\\:]+ operators = [\u0026#39;+\u0026#39;, \u0026#39;-\u0026#39;, \u0026#39;*\u0026#39;, \u0026#39;/\u0026#39;, \u0026#39;**\u0026#39;] print \u0026#39;|\u0026#39;.join(map(re.escape, sorted(operators, reverse=True))) # \\/|\\-|\\+|\\*\\*|\\* re.purge()\n 清除正则表达式缓存。 exception: re.error\n 当传递的正则表达式是非法的,或者在编译、匹配中发生错误,都会抛出此异常。 class: re.RegexObject\n 也就是 re.compile 产生的对象,re.RegexObject 实例的所有方法,和上边提到的同名函数功能类似(上边的函数内部其实就是先初始化一个 re.RegexObject 对象,然后调用它的同名方法),只是有细微的差别,这里不过多描述。 0x02 Match Objects 这里重新写一个章节来介绍 MatchObject,因为他是上边大部分函数的返回值,MatchObject 对象内部的方法和属性还是需要着重介绍一下的。\nMatchObject 的 bool 值总是 True,因此可以直接用来做 if 判断(因为 search()、match() 等如果没匹配到会返回 None)\n expand(template)\n template 可以通过 backreferences 来获取匹配到的值,作用类似于 re.sub() 那样。 a=re.search(\u0026#34;a(bb)\u0026#34;, \u0026#34;cabb\u0026#34;) a.expand(\u0026#34;www.\\g\u0026lt;1\u0026gt;.com\u0026#34;) # \u0026#39;www.bb.com\u0026#39; group([group1, \u0026hellip;])\n 返回一个或多个匹配到的子组。如果只有一个参数,那么返回值也是一个字符串;如果是多个参数,那么返回一个元组,每个参数代表元组中的一项;如果没有参数,group1 默认是 0,即返回整个匹配到的内容(不管 group 几是 0,对应的元组项都会返回整个匹配到的字符串)。 如果传递的参数是负值或者超过组的长度,则会抛出 IndexError 异常。 如果 pattern 中的某一个 group 没有匹配到内容,那么它对应的返回值就是 None;如果匹配到多次,那它会返回最后一次匹配到的值。 如果 pattern 中使用命名的组((?P\u0026lt;name\u0026gt;...)),那么参数可以是索引也可以是字符串,如果没有找到字符串对应的组,则会抛出 IndexError 异常。 m = re.match(r\u0026#34;(\\w+) (\\w+)\u0026#34;, \u0026#34;Isaac Newton, physicist\u0026#34;) m.group(0) # The entire match # \u0026#39;Isaac Newton\u0026#39; m.group(1, 2) # Multiple arguments give us a tuple. # (\u0026#39;Isaac\u0026#39;, \u0026#39;Newton\u0026#39;) m = re.match(r\u0026#34;(?P\u0026lt;first_name\u0026gt;\\w+) (?P\u0026lt;last_name\u0026gt;\\w+)\u0026#34;, \u0026#34;Malcolm Reynolds\u0026#34;) m.group(\u0026#39;first_name\u0026#39;) # \u0026#39;Malcolm\u0026#39; m.group(\u0026#39;last_name\u0026#39;) # \u0026#39;Reynolds\u0026#39; groups([default])\n 返回一个元组,元组包含所有匹配到的子组(从 1 开始到最后一个组)。 default 参数用于指定没有匹配到的组的默认值。 m = re.match(r\u0026#34;(\\d+)\\.?(\\d+)?\u0026#34;, \u0026#34;24\u0026#34;) m.groups() # Second group defaults to None. # (\u0026#39;24\u0026#39;, None) m.groups(\u0026#39;0\u0026#39;) # Now, the second group defaults to \u0026#39;0\u0026#39;. # (\u0026#39;24\u0026#39;, \u0026#39;0\u0026#39;) groupdict([default])\n 返回一个字典,key 是命名组的名称,value 是匹配到的内容。非命名的组不会被返回。 default 参数同 groups() m = re.match(r\u0026#34;(?P\u0026lt;first_name\u0026gt;\\w+) (\\w+)\u0026#34;, \u0026#34;Malcolm Reynolds\u0026#34;) m.groupdict() # {\u0026#39;first_name\u0026#39;: \u0026#39;Malcolm\u0026#39;} start([group]) \u0026amp;\u0026amp; end([group])\n 返回与 group 参数匹配的子字符串的开始和结束的索引。默认 group 是 0,意思就是匹配的整个字符串。 如果匹配到的组是一个空字符串,那么 m.start(groupN) == m.end(groupN)。 email = \u0026#34;tony@tiremove_thisger.net\u0026#34; m = re.search(\u0026#34;remove_this\u0026#34;, email) email[:m.start()] + email[m.end():] # \u0026#39;[email protected]\u0026#39; span([group])\n 返回 2-tuple (m.start(group), m.end(group))。默认 group 为 0,即返回整个匹配到的内容。 注意,如果 group 没有参与匹配,则为 (-1, -1)。 pos\n 传递给 RegexObject 对象的 search () 或 match () 方法的 pos 值。 表示正则表达式匹配引擎执行的开始位置 endpos\n 传递给 RegexObject 对象的 search () 或 match () 方法的 endpos 值。 表示正则表达式匹配引擎执行的结尾位置 lastindex\n 最后一个捕获匹配的组的索引,如果没有匹配到组,则返回 None。 针对 ab 字符串,pattern((a)b、((a)(b))、((ab)))中 lastindex == 1,pattern((a)(b))中 lastindex == 2 lastgroup\n 最后一个捕获匹配的组的名称,如果组没有名称或者根本没有匹配到组,则返回 None。 re\n re 是一个 正则表达式对象,它的 match() 或 search() 方法产生 MatchObject 实例 string\n 传递给 match() 或 search() 的字符串。 参考:\n📌 Regular expression operations\n",
"ref": "/blog/2019/python/python-standard-library-re/"
},{
"title": "程序员必须知道的常识",
"date": "",
"description": "程序员生涯中会遇到很多东西,而某些东西只需要记住就好了,不需要理解,常识就是这样",
"body": " 写在前面:\n这里总结的很多常识只是根据当时的计算机发展水平来预估的,给大家提供一个大致的参考,存在过时的可能(我会不定时更新)。\neg:redis 读操作最高能达到 10W QPS,这个只是针对当下的计算机水平和固定的 redis 版本评估出来的,过几年突破 100w 也是有可能的。\n 0x00 计算机 从内存读取 1M 的数据需要 250 微秒,SSD 需要 4 倍的时间,磁盘需要 80 倍的时间。 从内存顺序读的速度是 4G/s 从 SSD 顺序读的速度是 1G/s,内存的 1/4 从磁盘顺序读的速度是 30M/s,SSD的 1/30 一个月有 2.5M(250万)秒 位运算:右移一位相当于除以 2,左移一位相当于乘以 2 有一些十进制数的小数无法转换成二进制数(0.1) 内存和 CPU 都是集成电路(IC) 8-bit string 就是 ASCII 编码的字符串 0x01 网络 从 1G 的以太网顺序读的速度是 100M/s 同一个数据中心内数据往返 2000次/s 0x02 数据库 Redis 单机一般可以抗住读 100k QPS,写 80k QPS MySQL 单机一般可以抗住 5k 左右 QPS MySQL 单表大于 2000 万行或者大于 50-100G 就有压力 MySQL 单实例存储到达 3-3.8T 时, 一般情况下就已经到达极限了 Redis 集群单节点内存上限不能超过 20G 0x03 编程 带 log 的时间复杂度,一般都需要使用二分法或者二叉树等 使用循环可能性能更好,使用递归程序可能更容易理解 递归需要两个条件:基线条件(让递归停止)和递归条件(让递归继续执行) 两个算法的大 O 时间复杂度一样,并不代表这两种算法的效率一样(某个计算步骤可能非常耗时) 最短路径问题可以使用广度优先搜索算法完成 通常我们认为计算机可解决的问题只限于多项式时间内。而O(2ⁿ)、O(N!)这类非多项式级别的问题,其复杂度往往已经到了计算机都接受不了的程度。 动态规划要点是:找状态和状态转移方程,限制是:问题必须能够被分解成独立且不相关的子问题 ",
"ref": "/blog/2019/computer-basics/what-programmers-must-know/"
},{
"title": "Python 标准库:Queue",
"date": "",
"description": "Python 标准库 Queue 的用法总结",
"body": " Btw:\nPython3 中,Queue 模块已经重命名为 queue。Python3.7 中增加了一个 SimpleQueue 类,其他内容同 Python2。\n 0x00 Overview Queue 模块实现的是多生产者、多消费者的队列。因此它是线程安全的,可以在多线程下使用。因为 Queue 类内部实现了所有必须的锁。\nQueue 模块提供了三种类型的队列,三种类型的主要差异是获取数据的顺序不同。\n0x01 队列 class:Queue(maxsize=0)\n FIFO(先进先出)队列。\n maxsize 用来设置队列的最大容量,一旦到达最大值,插入操作就会被阻塞住,直到队列内的内容被消费。\n 如果 maxsize 小于等于 0,队列的容量是无限大。\n class:LifoQueue(maxsize=0)\n LIFO(先进后出)队列,类似于栈。\n 其他规则同 Queue。\n class:PriorityQueue(maxsize=0)\n 优先队列,内部使用 heapq 实现。\n 其他规则同 Queue。\n 优先返回优先级低的数据,典型的数据模式是一个元组:(priority_number, data)\n exception:Empty\n 在一个空队列调用非阻塞 get() 或者 get_nowait() 时会抛出此异常。 exception:Full\n 在一个容量达到最大值的队列调用非阻塞 put() 或者 put_nowait() 时会抛出此异常。 0x02 Queue 对象介绍 LifoQueue,PriorityQueue 都是继承自 Queue\n Queue.qsize()\n 返回队列的近似大小,不能保证读写不会被阻塞。 Queue.empty()\n 判断队列是不是空,返回 True or False。不能保证读写不会被阻塞。 Queue.full()\n 判断队列是不是满的,返回 True or False。不能保证读写不会被阻塞。 Queue.put(item[, block[, timeout]])\n 将一个元素插入到队列中。如果 block=True 并且 timeout=None,如果队列满的话会阻塞,直到有空位置。\n 如果 timeout 是正数 x,它会阻塞 x 秒,这段时间内如果没有空位置,则会抛出一个 Full 异常。\n 如果 block=False,如果队列没有空位置,则会立即抛出一个 Full 异常。\n Queue.put_nowait(item)\n 等价于 put(item, False). Queue.get([block[, timeout]])\n 从队列移除并返回数据。如果 block=True 并且 timeout=None,如果队列为空的话会阻塞,直到有数据可用。\n 如果 timeout 是正数 x,它会阻塞 x 秒,这段时间内如果没有数据,则会抛出一个 Empty 异常。\n 如果 block=False,如果队列没有数据,则会立即抛出一个 Empty 异常。\n Queue.get_nowait()\n 等价于 get(False) Queue.task_done()\n 表示前一个排队的任务已经完成,常用于队列的消费线程。\n 调用 get() 获取一个任务以后,紧接着调用 task_done(),就是告诉队列:正在处理的任务已经完成。\n 如果阻塞在 join() 处,需要重新调用 task_done() 将所有任务处理完成。\n Queue.join()\n 调用以后就进行阻塞,直到所有的任务都被获取并处理完(task_done())。 一个消费线程调用 task_done(),未完成任务的数量就会减一。减到零以后就会停止阻塞。 参考:\n📌 Queue — A synchronized queue class\n",
"ref": "/blog/2019/python/python-standard-library-queue/"
},{
"title": "音乐:Photograph",
"date": "",
"description": "黄老板很好听的一首歌,MV 记录了 Ed Sheeran 从小到大的片段",
"body": "0x00 Photograph - Ed Sheeran document.getElementById(\"video-bilibili\").style.height=document.getElementById(\"video-bilibili\").scrollWidth*0.5+\"px\"; ",
"ref": "/awesome/ed-sheeran-photograph-mv/"
},{
"title": "TED演讲:什么是区块链?",
"date": "",
"description": "一个很火的TED演讲让你真正看懂区块链",
"body": "0x00 《什么是区块链?》 document.getElementById(\"video-bilibili\").style.height=document.getElementById(\"video-bilibili\").scrollWidth*0.5+\"px\"; ",
"ref": "/awesome/what-is-block-chain/"
},{
"title": "抛弃 Python2,从此使用 Python3",
"date": "",
"description": "Python3 ",
"body": " 为什么要使用 Python3?我 2 用的好好的。(py 版本演进过程;2018 年开发者问卷调查;)\nPython 2 3 的特性对比\n到底写 2 还是写 3,还是两个都写。(如果是自己的项目,只写 py3,如果是提供的第三方库,需要写 3 代码来兼容 2。为什么不是写 2 的代码兼容 3 呢?2 孔的插座好比 Python2,3 孔的好比 Python3,本来是 2 孔的充电器插 2 孔的电源,3 孔的插 3 孔的。现在假如 2 孔的插座快淘汰了,你是愿意给 3 孔的充电器安装一个 2 孔的转接头,让他可以用使用 2 孔的插座呢,还是另外一种做法)\n如何逐步迁移到 Python 3(不迁移的话,你永远在使用 py2,因为你迈不出第一步)(需要注意很多老库不支持 py3)\n迁移工具有哪些\n如何部署 Python 2 和 Python3 的环境,并且还有虚拟环境\n 0x00 为什么要升级到 Python3 Python2\n Python2 算是一种比较古老的语言 2010 年 7 月,发布 2.7.0 版本的时候,就宣布了以后不会再发布 Python 2.x 大版本。参考 PEP 404 2020 年 1月 1 日以后,Python 官方不再支持 Python2。最初是打算 2014 年停止支持,发现大多数人还是没有迁移到 Python3,所以推迟到 2020 年。参考 PEP 373 Python3\n Python3 是一种比较现代化的语言 第一个版本 3.0.0,发布于 2008 年 12 月 初期的 3.x 版本不稳定,直到 3.3 版本(2012 年 9 月发布)以后 Python2 语言在最初设计的时候,就存在很多问题(Unicode、print \u0026amp; exec 都是语句、整数使用了地板除、rang等),在后来版本迭代的时候发现向后兼容的成本大于新写一门语言,所以就果断放弃了 Python2。\n为什么要推迟停止支持 Python2 的时间?我个人觉得是 Python3 早期没有太多的库来支持,如果我的项目想迁移到 Python3,但是我依赖的库只支持 Python2,那我肯定是放弃升级 Python3 的。\nBut now,时代不一样了,95% 的常用库都已经支持 Python3 了,所以,你还在等什么呢?\n另外,根据 2017 和 2018 年的 Python 开发者调查报告显示,2018 年使用 Python3 的人数高达 84%,2019 年的报告还没出来,占比肯定 会更高。\n0x01 主要差异对比 Python2 Python3 print print 是语句 函数 整数除法 地板除 返回浮点型 input 转换成输入的类型 传入的值总是文本类型(类似 Python2 中的 raw_input) Round 返回浮点型数字 返回指定的精度 变量泄漏 存在变量泄漏 不存在 range 直接返回列表 返回生成器(类似 Python2 中的 xrange) Exception as 关键字可有可无 as 关键字必须存在 生成器的 .next 方法 可以使用 不能使用 ASCII \u0026amp; Unicode \u0026amp; Byte 有 ASCII string 和 Unicode 类型,无 Byte 类型 有 Unicode 和 Byte 类型 不再返回 list:\n zip() map() filter() dictionary’s .keys() method dictionary’s .values() method dictionary’s .items() method 0x02 如何写兼容 Python2 \u0026amp; 3 的代码 首先需要明确一个问题:写 Python2 代码来兼容 Python3,还是写 Python3 代码来兼容 Python2?\n 历史经验来看,最佳实践是写Python3 代码来兼容 Python2\n 打一个不太恰当的比方:2 孔的插座好比 Python2 解释器,3 孔的好比 Python3 解释器,本来是 2 孔的充电器(Python2 代码)插 2 孔插座(Python2 解释器),3 孔(Python3 代码)的插 3 孔插座(Python3 解释器)。现在假如 2 孔的插座(Python2 解释器)快被淘汰了,你是愿意给 3 孔的充电器(Python3 代码)安装一个 2 孔的转接头(six library),让他可以同时使用 2 孔(Python2 解释器)和 3 孔插座(Python3 代码)呢?还是给 2 孔充电器(Python2 代码)安装一个 3 孔的转接头(six library)?\n 当然是给 3 孔的充电器(Python3 代码)安装 2 孔的转接头(six library)。因为 2 孔的插座(Python2 解释器)被淘汰以后,我们完全可以不使用转接头了。否则必须一直依赖转接头(six library)。\n python-future 库是用来弥补 Python2 和 Python3 兼容性的库,也就是一个转接头。\nsix 也是一个解决 Python2 和 Python3 兼容性的库\nPython 内置 __future__\n这三者的区别:\n __future__ Python 内置的兼容模块,提供基础的兼容特性(print 函数、Unicode 等) 0x03 升级工具 0x04 安装 Python 2 \u0026amp; 3 的环境 参考:\n📌 Python 2 or 3?\n📌 Should I use Python 2 or Python 3 for my development activity?\n📌 PEP 373 \u0026ndash; Python 2.7 Release Schedule\n📌 Why was Python 3 made incompatible with Python 2?\n📌 Cheat Sheet: Writing Python 2-3 compatible code\n📌 Python 2 vs Python 3: Key Differences\n📌 Python2 vs Python3 | Syntax and performance Comparison\n📌 Best Practices for Compatible Python 2 and 3 Code\n📌 The key differences between Python 2.7.x and Python 3.x with examples\n📌 Writing cross-compatible Python 2/3: Difference between __future__, six, and future.utils?\n",
"ref": "/blog/2019/python/why-upgrade-python3/"
},{
"title": "视频:林轩田机器学习基石",
"date": "",
"description": "台湾大学林轩田老师在 coursera 上开设的机器学习经典课程",
"body": "0x00 《机器学习基石》 document.getElementById(\"video-bilibili\").style.height=document.getElementById(\"video-bilibili\").scrollWidth*0.5+\"px\"; ",
"ref": "/awesome/machine-learning-foundation/"
},{
"title": "Python 与 Golang 语法特性对比",
"date": "",
"description": "Python 程序员入门 Golang,通过表格对比,加强记忆,也可以作为一个语法速查表",
"body": "0x00 缘起 之前整体把 golang 的语法过了一遍,花了大概几个小时。\n然鹅,2 天没写 go 代码,语法就忘完了,还得回过头去查一次,真是浪费时间。\n所以,我就想到了这个联想记忆,通过熟悉的东西,来记住陌生的东西。\n0x01 语法特性对比 参考:go 语言之旅\n Python Go 包声明 文件名 文件内定义:package main 引入包 import json import \u0026ldquo;math/rand\u0026rdquo; 模块对象暴露 __all__ 包内对象首字母大写 函数 def 关键字 func 关键字 函数多指返回 return x, y return x, y(特例:func split(sum int) (x, y int) { x = sum * 4 / 9; y = sum - x; return }) 变量 动态类型 关键字 var,在变量名后声明(多个变量类型相同时,前边类型可以忽略) 变量初始化 直接赋值 如果提供初始值,则不需要指明类型 短变量声明 / :=(函数外必须使用 var 关键字) 基本类型 int、string、bool、list、dict、tuple bool、string、int[8, 16, 32, 64]、uint[8, 16, 32, 64]、uintptr、byte(uint8 的别名)、rune(int32 的别名;表示一个 Unicode 码点)、float32、float64、complex64、complex128 零值 / 没有赋初值的变量默认为零值(int 为 0;bool 为 false;字符串为 \u0026lsquo;';指针为 nil;切片为 nil,它的长度和容量为 0,且没有底层数组;映射为 nil,nil 映射既没有键,也不能添加键;) 类型转换 同 go 语言 T(v) 将值 v 转换为类型 T(不同类型直接赋值需要显式转换) 常量 与变量相同 关键字 const,不能使用 := 语法(常量不指明类型的话,会根据上下文自动识别其类型) for for i in [1, 2, 3]: for i:=0; i\u0026lt;10; i++ {xxx}(初始化语句和后置语句是可选的) while while True: for sum \u0026lt; 1000 {xxx} 死循环 while True: for {xxx} if if i \u0026lt; 10: xxx else: xxx if v := math.Pow(x, n); v \u0026lt; lim {xxx} else {xxx}(if 和 for 语句类似,可以再 if 关键字后加一个初始化语句,这个变量的作用域是 if 和 else 作用域内) switch 无,使用字典代替 switch os := runtime.GOOS; os {case \u0026ldquo;abc\u0026rdquo;: xxx} (运行 switch 关键字后的赋值语句,然后运行匹配成功的那个 case,case 关键字后的值,无需为常量,也可以是函数等)(switch {} 等于 switch true {}) 推迟调用 无 关键字 defer,作用是将指定的调用推迟到外层函数返回以后执行(defer 的调用会被压入栈中,所以先声明的后调用) 指针 无,变量名都是引用 声明指针变量时,给类型前加 *;\u0026amp; 可以获取变量的地址;*p 可以通过指针设置或读取值 (var p *int,p = \u0026amp;i) 结构体 类 type Vertex struct {X int; Y int} v2 = Vertex{X: 1}(一组字段的集合,字段通过点号来访问,也可以通过指针来访问,指针前不需要加 *,编译器自己会处理优化) 数组 list [n]T 表示拥有 n 个 T 类型的值的数组 切片 [x:y](生成一个新的数组) 与 Python 类似的语法,可以忽略 x 和 y 的值,同样也是左闭右开的取值范围(切片生成的变量只是之前数组的引用)(切片有长度和容量:长度是包含的元素个数,容量是从第一个元素到底层数组的最后一个元素的个数。切片时左范围会改变数组容量)(可以使用 make 函数动态创建一个数组的切片:b := make([]int, len, cap)) 数组添加元素 append 方法 append 函数:func append(s []T, vs \u0026hellip;T) []T。s 为切片,将新的值添加到切片最后,如果 s 的底层数组太小,会新分配一个更大的数组,然后返回的切片指向这个新数组 range 返回一个数组 for i, v := range pow {xxx},使用在 for 循环中,i 是当前元素在数组中的下标,v 是该下标对应的元素副本(这里副本是指针还是什么,有待查询?\u0026ndash;副本指的是另外一个值一样,但是内存地址不一样的变量)(可以使用 _ 来忽略某一个变量,也可以手动忽略最后有一个变量。感觉有点像 js-es6 的语法) 映射 dict 关键字 map,make 函数用来创建给定类型的映射并初始化:m = make(map[string]int)。(修改和获取键对应的值得方式和 Python 一样,删除使用 delete(m, key),检测某个键是否存在 elem, ok = m[key],ok 标识是否存在,若不存在 elem 是该映射元素类型的零值) 函数也是值 是 是的,和 Python 一样,可以作为函数参数进行传递或者作为返回值 闭包 内嵌函数引用外部函数的变量,并且外部函数返回该内部函数 和 Python 闭包概念一样 方法 类中的方法 为结构体定义的函数叫做方法。方法是一类带有特殊接受者(就像 Python 中的 self)参数的函数:func (v Vertex) Abs() float64 {xxx}。当使用 Vertex 结构体的实例变量调用 Abs 方法(a := Vertex{3, 4}; a.Abs())的时候,Abs 方法中的 v 就等于这里的 a 变量。(结构体的类型定义和方法声明必须在同一包内;不能为内建类型声明方法)(方法的接受者也可以是该类型的指针,这种更常用,因为指针可以修改结构体的值)(函数与方法的小区别:函数的参数如果是指针类型,那么调用的时候必须传一个指针类型;但是方法不一样,即使方法需要一个指针接受者,但是 a.Abs() 这样调用也是可以的,其中 a 不是指针类型,这是因为 go 内部将其隐士修改为 (\u0026amp;a).Abs(),同样的接受者为值类型的话,使用值和指针调用都可以。总结一下:不管方法的接受者是指针还是值类型,它的调用者可以是指针和值中的任意一个,通常选择使用指针作为接受者,因为指针接受者可以改变结构体内部的值,另外在每次调用的时候不需要复制该值) 接口 继承 接口是一种类型,一组方法签名的集合(可以理解为父类,父类定义了很多接口,子类(这里相当于结构体)必须实现所有接口,才能满足 is-a 的关系)。type Abser interface {Abs() float64}。接口也是值,也可以当做参数或者返回值,它的值是一个元组 (value:值, type:类型)。nil 值仍然可以调用接口方法。为初始化的 nil 接口值,调用方法会报运行时错误,因为不知道需要执行哪个方法。 类型判断 type 函数 s, ok := i.(string),s 是底层值,ok 表示是否是 string 类型 协程 yield、gevent goroutine 是由 go 运行时管理的轻量级线程。语法:go f(x, y, z)。f、x、y、z 的求值发生在当前的 goroutine 中,f 的执行发生在新的 goroutine 中。(goroutine 在相同的地址空间运行,因此访问共享的内存时必须进行同步,可以使用 sync 包。) 信道 / 带有类型的管道,使用 make 函数创建:ch := make(chan int, 100),第二个参数是可选的,表示信道的缓冲区大小,缓冲区满了发送数据会阻塞,缓冲区为空接收数据会阻塞;从信道发送接收内容:ch \u0026lt;- v; v := \u0026lt;-ch。默认情况下,在另一端未准备好的情况下,另一端是阻塞的状态(隐式同步)。(发送者可以通过 close 函数来关闭一个信道,表示没有任何内容需要发送了,注意,只有发送者才能关闭信道,接受者不可以关闭。接收时 v, ok := \u0026lt;-ch,信道关闭后,ok 会被置为 false。)(for i := range c 可以不断从信道读取数据,直到信道关闭。信道和文件不同,通常情况下不用主动关闭它们,除非需要告诉接受者不会再给信道发送数据了,比如这里需要通过 rang 来终止循环。) select select:io 多路复用的 Windows 第三方库 select 语句可以通过一个 goroutine 来等待多个信道通信操作,语法:select {case c \u0026lt;- a:xxx; case \u0026lt;-quit:xxx; default:xxx}。(select 会一直阻塞,直到有分支可以继续执行,如果不想阻塞的话,可以定义 default 分支;如果多个分支都可以执行,会随机选择一个分支执行。) 锁 threading.Lock sync.Mutex ",
"ref": "/blog/2019/go/go-compare-python/"
},{
"title": "Hugo 配置 staticman 静态评论平台",
"date": "",
"description": "staticman 使用的坎坷之路",
"body": "0x00 起因 我使用的 hugo 主题 hugo-future-imperfect,默认支持 disqus 和 staticman 的评论方式。\n奈何有一些不可描述的原因,disqus 无法访问,遂转投 staticman 怀抱,这才开启了踩坑之路。\n0x01 太年轻 准备按照 staticman 的 Getting started 一步一步执行。\n可谓是出师不利,第一步就过不去:无法把 staticmanapp 账户添加到 Collaborators 中。\n\n随后访问 https://api.staticman.net/v2/connect/LoveXiaoLiu/blog-comment 也是各种报错。\n\n于是去 staticman 的 issue 区找找,果然很多人遇到了这个问题。\n出现这个问题的原因是由于使用 staticman 的人太多了,导致部署的公共 staticman app 达到了 Github 的限制(Github 限制每个用户每小时只能调用 API 5K 次)。\n 为什么会达到这个限制呢? staticman 的工作方式是这样的,你将 staticman 添加到你的博客系统以后,需要将 staticmanapp 账户添加到你的 repo Collaborators 中。当有人在你的博客中提交评论,staticman 会将这些评论内容通过公开的 staticman app,以提交代码的方式提交到你的 repo 中。问题就出在这里,提交代码的账户就是 staticmanapp,随着 staticman 的用户越来越多,staticmanapp 肯定会超过这个限制的。 目前这个账户已经被注销。 针对这个问题,维护人员咨询了 GitHub 官方,官方回复让他创建一个 GitHub App 参考 issues 243 0x02 自己动手 本来想自己动手搭建一个 staticman app(参考这个教程)\n奈何已经有轮子了,何必自己造一个呢?\n是的,已经有人写好了 GitHub App for staticman,所以可以直接拿来用:\n 去这里 https://github.com/apps/staticman-net 给自己的 GitHub 账号安装此 APP 将评论的 post 请求地址换成 https://dev.staticman.net/v3/entry/github/[USERNAME]/[REPOSITORY]/[BRANCH] 即可 需要增加评论审核功能的话,在 staticman.yml 文件将 moderation 设置为 true ",
"ref": "/blog/2019/blog/hugo-plus-staticman/"
},{
"title": "HTTP 协议透彻解析",
"date": "",
"description": "从入门到放弃的讲解 HTTP 协议",
"body": " keywords:\n http 协议是什么(历史、未来发展) http 协议的传输过程 http 协议包内容 http 协议的问题 0x00 HTTP 协议是什么 btw:什么是协议? 协议不光在计算机中使用,我们平时生活中经常会用到协议。比如说两个网友奔现,双方都不认识,那就必须定义一个约定或者协议:男方来了必须穿黑色牛仔裤,手里拿一束花,女方必须穿白色上衣,背红色包包。这样,才能互相认识,否则就无法完成奔现。\n 先参照上面的例子简单说下 HTTP 协议:女方就好比是 Client 端,男方就好比是 Server 端。Client 和 Server 进行通信的时候双方不认识,那就必须遵守某种协议才能进行交互。这个协议就是 HTTP 协议。\n接下来用正式的、官方的解释说下 HTTP 协议:HTTP 协议是一种通过 Web 获取资源(HTML、CSS、文本等)的协议。它是数据交互的基础,是一种 C-S 结构的协议。\n官方的解释很清楚,HTTP 协议的作用就是在网络上获取资源。\nKey point:\n HTTP 交互的是单独的 message,而不是数据流(stream of data) HTTP 是应用层协议,通过 TCP 或者 TLS 加密的 TCP 连接发送数据 HTTP 协议的高度可扩展性,使它能传输任何类型的文件(video、imag 等) Client 与 Server 中间可以有多层代理 HTTP 协议依靠 HTTP headers 可以自由的扩展功能 HTTP 协议是无状态的(stateless,同一个连接先后发送的两个请求是没有联系的),但是是有会话状态的(not sessionless,通过 HTTP header 的可扩展性,HTTP cookie 支持有状态的会话) 历史 早在 1990 年,HTTP 协议就被设计出来了,它是一个高度可扩展的协议,灵活性高,所以发展的很迅速。\norigin constraint 在 2010 年被放宽限制,通过指定 specific header 可以跨域访问。\nHTTP/1.0\n 一个 HTTP 请求建立一个 TCP 连接 HTTP/1.1\n 增加了持久连接(persistent connections),使用 HTTP Connection header 来控制 TCP 连接 pipelining,这个很难实现(现在有没有实现待确认?) HTTP/2\n 将 HTTP 消息封装到 frame 中 传输方式更加优化,使用多路复用技术(multiplexing),即一条连接发送多个 HTTP 请求。与 HTTP/1.1 主要不同就是可以在同一条 TCP 连接上发送/接收多个请求,而不需要等待上一个请求返回,HTTP/1.1 是顺序请求的。 未来 针对 transport protocol 在做一些实验:Google 正在做 QUIC 参考:\n📌 HTTP BY MDN\n",
"ref": "/blog/2019/web/http-overview/"
},{
"title": "开发 Python 第三方包,上传到 PyPI",
"date": "",
"description": "如何开源一个 Python 第三方包",
"body": "0x00 初始化 skeleton 开源一个 Python 第三方包,需要配置很多额外的东西(pip 需要的 setup.cfg,CI/CD 需要的 .travis.yml 等),这些东西完全没必要自己来逐一创建。\n所以我们需要一个脚手架来快速创建项目的 skeleton。这里我推荐使用 cookiecutter-X 系列工具。\n我这里开发 pypi 包,使用 cookiecutter-pypackage 模板。\ncookiecutter https://github.com/audreyr/cookiecutter-pypackage.git 0x01 生态配置 创建完项目 skeleton 以后,需要关联 Travis 账号、ReadTheDocs 账号等。步骤如下:\n 将 repo 添加到 GitHub 中 将该 repo 添加到 Travis CI 中(需要注册 Travis 账号) 安装 dev requirement.txt 到虚拟开发环境中:pip install -r requirements_dev.txt 注册项目到 PyPI 中(需要注册 PyPI 账号) 生成 tar\u0026amp;wheel 包:python setup.py sdist bdist_wheel 上传包到 PyPI 中:python -m twine upload dist/* 将该 repo 添加到 ReadTheDocs 中 0x02 迭代开发 更新项目代码,开发新 features 使用 bumpversion 升级版本:bumpversion --current-version 0.1.0 minor setup.py 上传包到 PyPI 中 ",
"ref": "/blog/2019/python/python-package-pypi/"
},{
"title": "",
"date": "",
"description": "",
"body": "template: 使用模板时,删除这句话。+++ title = \u0026ldquo;标题\u0026rdquo; description = \u0026ldquo;文章简述\u0026rdquo; author = \u0026ldquo;gra55\u0026rdquo; date = \u0026ldquo;2021-12-20\u0026rdquo; categories = [\u0026ldquo;计算机基础\u0026rdquo;] tags = [\u0026ldquo;基础\u0026rdquo;, \u0026ldquo;2021\u0026rdquo;] [[images]] src = \u0026ldquo;img/2019/12/macau_back_china.jpg\u0026rdquo; alt = \u0026ldquo;1999年12月20日葡萄牙结束统治澳门,中国政府恢复对澳门行使主权,中华人民共和国澳门特别行政区成立\u0026rdquo; stretch = \u0026ldquo;horizontal\u0026rdquo; +++\n 引用:\n 0x00 简述 0x01 章节一 0x02 章节二 0x03 章节三 0xff 参考 ",
"ref": "/blog/_template/"
},{
"title": "About Gra55",
"date": "",
"description": "",
"body": "I'm a full stack engineer, working for 6 years.\nI like playing basketball 🏀, and I like Kobe too, because he is a man with strong will, we call it MAMBA spirit.\n",
"ref": "/about/"
},{
"title": "Contact",
"date": "",
"description": "",
"body": "",
"ref": "/contact/"
}]