-
Notifications
You must be signed in to change notification settings - Fork 1
/
search.xml
649 lines (349 loc) · 691 KB
/
search.xml
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
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>库/头文件搜索路径相关GCC参数及环境变量</title>
<link href="/2018/07/01/Development/Language/C_CPP/Compiling/gcc-header-library-search/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><hr><p>在Linux系统中,命令行下编译C/C++代码是不二选择,<strong>GCC</strong>则是编译工具首选,提供了丰富的编译、调试特性,功能十分强大。本文介绍了使用<strong>GCC</strong>编译时,与头文件及库文件搜索路径相关的命令行参数及环境变量。</p><a id="more"></a><p>高级语言,因为其抽象性所以用户友好,但也正因于此,需要翻译成机器指令才能让CPU理解并最终执行。<br>对于C/C++程序而言,翻译是一个过程,包含如下步骤:</p><ol><li>预处理(<code>*.c</code> -> <code>*.i</code>)</li><li>编译(<code>*.i</code> -> <code>*.s</code>)</li><li>汇编(<code>*.s</code> -> <code>*.o</code>)</li><li>链接(<code>*.o</code> -> <code>ELF</code>)</li></ol><p>链接最后生成一个二进制的可执行文件,装载入内存方可执行。各个步骤的具体细节正在学习之中,将在之后的博文中介绍。</p><p>本文重点关注于介绍在使用<strong>GCC</strong>编译时常用的一些命令行参数及所需配置的环境变量,如指定头文件搜索路径、静态库搜索路径、动态库搜索路径等。</p><h2 id="实验平台"><a href="#实验平台" class="headerlink" title="实验平台"></a>实验平台</h2><hr><p>操作系统:Ubuntu 16.04.4<br>GCC版本:5.4.0</p><h2 id="静态库"><a href="#静态库" class="headerlink" title="静态库"></a>静态库</h2><hr><p>静态库文件命名以<code>lib</code>开头,后缀为<code>.a</code>。<br>静态库代码在编译时期即载入可执行程序,但可能导致可执行文件的体积过大。</p><p>由<code>.o</code>文件创建静态库:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ gcc -c hello.c</span><br><span class="line">$ ac cr libhello.a hello.o # 静态库名为libhello.a</span><br></pre></td></tr></table></figure></p><p>编译时链接静态库,假设静态库放在当前目录(.)下:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ gcc -o hello main.c -L. -lhello</span><br></pre></td></tr></table></figure></p><p>注意:<code>-lhello</code>放置的位置很重要,因为<code>main.c</code>中用到了<code>libhello.a</code>中定义的方法<code>hello()</code>,则必须将<code>-lhello</code>放在<code>main.c</code>之后,否则<strong>ld</strong>报错:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ gcc -o hello -L. -lhello main.c </span><br><span class="line">/tmp/xxxxxxxx.o: In function 'main':</span><br><span class="line">main.c:(.text+0xa): undefined reference to 'hello'</span><br><span class="line">collect2: error: ld returned 1 exit status</span><br></pre></td></tr></table></figure></p><p>链接时,链接器<strong>ld</strong>会将静态库中的相关代码全部放入最终形成的可执行文件中,因此即使在得到可执行文件后立即删除静态库文件,执行可执行文件时也不会有任何问题,因为可执行文件所依赖的静态库相关代码全部以副本形式保存在了可执行文件中。</p><h2 id="动态库"><a href="#动态库" class="headerlink" title="动态库"></a>动态库</h2><hr><p>动态库文件命名以<code>lib</code>开头,后缀为<code>.so.x.y</code>,其中<code>x</code>为主版本号,<code>y</code>为副版本号。<br>共享库的代码是在可执行文件运行时才载入内存的,在编译过程中仅是简单地引用,因此可执行文件的体积较小。</p><p>C与C++的标准库均为动态库,在Ubuntu 16.04.4下,路径如下:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">/lib/x86_64-linux-gnu/libc.so.6</span><br><span class="line">/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21</span><br></pre></td></tr></table></figure></p><p>由<code>.o</code>文件创建动态库:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ gcc -fPIC -c hello.c </span><br><span class="line">$ gcc -fPIC -shared libhello.so.1.2 hello.o # 要用"-fPIC"内容无关代码这一选项,必须在生成hello.o文件时也要指定该选项</span><br></pre></td></tr></table></figure></p><p>编译时链接动态库,假设动态库放在当前目录(.)下:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ gcc -o hello main.c -L. -lhello</span><br></pre></td></tr></table></figure></p><p>注意:<code>-lhello</code>放置的位置很重要,理由和静态库部分一样。</p><p>执行:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ ./hello</span><br></pre></td></tr></table></figure></p><p>发现再次报错:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ ./hello: error while loading shared libraries: libhello.so.1.2: cannot open shared object file: No such file or directory</span><br></pre></td></tr></table></figure></p><p>根据错误提示可知:在运行时未能找到动态库<code>libhello.so.1.2</code>。你或许会问,编译都成功了,说明链接器确实找到了所依赖的动态库的位置,为什么在运行时却又找不到了呢?</p><p>这是因为:可执行文件在被装载运行时,会在系统默认的相关路径下去查找所依赖的动态库文件,如<code>/usr/lib</code>和<code>/lib</code>等目录。若能在这些路径下找到库,则将其装载,程序成功运行。否则就将出现上述错误导致程序终止。</p><p>解决方案:配置环境变量<code>LD_LIBRARY_PATH</code>,用于指定运行时的动态库的实际路径,使得程序在运行时能够找到并装载动态库。<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:. # 在当前终端中临时配置环境变量,新增当前目录(即".")为动态库运行时搜索位置</span><br></pre></td></tr></table></figure></p><p>这里其实还有另外一个环境变量<code>LIBRARY_PATH</code>,用于指定编译链接时的动态库的实际路径,效果类似于后文马上要介绍的<strong>GCC</strong>参数<code>-Ldir</code>,但两者同时存在时,会优先搜索<code>-Ldir</code>指定的路径。</p><p>此外,跟动态库搜路路径相关的还有一个配置文件<code>/etc/ld.conf</code>,也会指定一些库搜索路径,在Ubuntu 16.04.3系统下,该文件初始内容为:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">include /etc/ld.so.conf.d/*.conf</span><br></pre></td></tr></table></figure></p><p>进入到目录<code>/etc/ld.so.conf.d</code>下,发现有如下5个<code>.conf</code>文件:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">fakeroot-x86_64-linux-gnu.conf # /usr/lib/x86_64-linux-gnu/libfakeroot</span><br><span class="line">libc.conf # /usr/local/lib</span><br><span class="line">x86_64-linux-gnu.conf # /lib/x86_64-linux-gnu 和 /usr/lib/x86_64-linux-gnu</span><br><span class="line">x86_64-linux-gnu_EGL.conf -> /etc/alternatives/x86_64-linux-gnu_egl_conf</span><br><span class="line">x86_64-linux-gnu_GL.conf -> /etc/alternatives/x86_64-linux-gnu_gl_conf</span><br></pre></td></tr></table></figure></p><p>编辑<code>/etc/ld.so.conf</code>文件,在其中增加一行本机中<strong>boost</strong>库所在路径,如下:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">/include /etc/ld.so.conf.d/*.conf</span><br><span class="line">/usr/local/boost-1.55/lib</span><br></pre></td></tr></table></figure></p><p>并执行:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo ldconfig # 根据配置文件/etc/ld.so.conf内容更新动态链接库搜索路径</span><br></pre></td></tr></table></figure></p><p>即可新增该路径为链接器<strong>ld</strong>的动态库的搜索路径,这样依赖于<strong>boost</strong>的可执行文件在运行就可定位到其动态库,装载后成功运行。</p><p>至于环境变量<code>LD_LIBRARY_PATH</code>与配置文件<code>/etc/ld.so.conf</code>之间的关系,如是同时生效先后加载还是存在覆盖,暂时没有查到太权威的资料,因为本人就曾遇到过配置了<code>LD_LIBRARY_PATH</code>环境变量,并且<strong>ldd</strong>已经准确定位到某可执行文件所依赖的动态链接库的位置,但在运行该可执行文件时依然报错:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ ./netsight: error while loading shared libraries: libboost_thread-mt.so.1.55.0: cannot open shared object file: No such file or directory</span><br></pre></td></tr></table></figure></p><p>后来我将该<strong>boost</strong>库的路径<code>/usr/local/boost-1.55/lib</code>增加到配置文件<code>/etc/ld.so.conf</code>中,执行<code>sudo ldconfig</code>后,再次运行才成功。所以友情提示大家,凡是遇到运行时找不到动态库的问题时,最直接也是最有效的方法就是新增库搜索路径到配置文件<code>/etc/ld.so.conf</code>,一定记得最后<code>sudo ldconfig</code>一下。</p><p>注:在同名静态库和动态库同时存在时,<strong>GCC</strong>编译时默认优先链接动态库,但可通过<code>-static</code>选项强制其链接静态库。</p><p><strong>GCC</strong>中与链接器<strong>ld</strong>相关的参数,还有一些参数也非常重要:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">-rpath=dir,增加运行时库的搜索路径,即动态库的搜索路径,在编译链接和运行时均有效。</span><br><span class="line">-rpath-link=dir,增加运行时库的搜索路径,即动态库的搜索路径,仅在编译链接时有效。</span><br></pre></td></tr></table></figure></p><p>链接器<strong>ld</strong>会按照下列顺序搜索所需动态库:</p><ol><li>由”-rpath-link=dir”指定的目录dir</li><li>由”-rpath=dir”指定的目录dir</li><li>由环境变量LD_RUN_PATH指定的目录</li><li>由环境变量LD_LIBRARY_PATH指定的目录</li><li>由环境变量DT_RUNPATH或DT_RPATH指定的目录</li><li>默认搜索路径,通常为/lib和/usr/lib</li><li>/etc/ld.so.conf中指定的目录</li></ol><h2 id="库文件搜索路径"><a href="#库文件搜索路径" class="headerlink" title="库文件搜索路径"></a>库文件搜索路径</h2><hr><p><strong>GCC</strong>默认的动态库搜索路径<code>SEARCH_DIR</code>可以通过<strong>ld –verbose</strong>命令查看。</p><p><strong>GCC</strong>在编译文件时通过如下参数可新增库的搜索路径:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">-Ldir:链接时在目录dir中查找由"-l"参数指定的库</span><br><span class="line">-llibray:链接时搜索名称为library的库,该选项在编译整个命令中的位置会表现出不同(make difference),链接器ld会按照其书写的顺序搜索和处理库和目标文件(*.o)</span><br></pre></td></tr></table></figure></p><p>使用实例参考上一节中在编译<code>main.c</code>新增<code>libhello.a</code>或<code>libhello.so.1.2</code>库的路径:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ gcc -o hello main.c -L. -lhello</span><br></pre></td></tr></table></figure></p><p>那么我们如何知道一个可执行文件运行时依赖哪些库呢?<br>可以使用<strong>ldd</strong>命令,它可以查看一个可执行程序依赖的共享库,该命令不仅会输出共享库的名称,还有共享库在系统中的所在路径(也是运行时的动态库搜索路径)和在可执行文件(ELF)中的地址(0x0000xxxxxxxxxxxx,16x4=64位内存地址),对C++的标准库执行该命令,会发现其依赖于C标准库<code>libc.so</code>(即glibc)。</p><h2 id="头文件搜索路径"><a href="#头文件搜索路径" class="headerlink" title="头文件搜索路径"></a>头文件搜索路径</h2><hr><p>在C/C++程序中,头文件(<em>.h)即对外暴露的API接口,使用者通过在程序开始处<code>#include</code>头文件,即可使用头文件中声明的类与方法,而头文件中仅有类和方法的声明,定义则通常在打包在库中的源文件</em><code>*.c</code>,<code>*.cpp</code>或<code>*.cc</code>中。</p><p><strong>GCC</strong>头文件搜索路径相关参数:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">-I dir,增加头文件搜索路径dir,"-I"指定的目录会在系统默认搜索路径之前被搜索</span><br><span class="line">-Idir,增加头文件搜索路径dir,是头文件搜索的第一顺位选择,多个"-I"参数则按照从左到右的顺序依次进行搜索</span><br><span class="line">-isystem dir,增加头文件搜索顺序dir,顺序排在在"-I"之后,但是在系统默认搜索路径之前</span><br></pre></td></tr></table></figure></p><p><code>-I dir</code>是”Options Controlling the Preprocessor”,而<code>-Idir</code>是”Options for Directory Search”,效果等同。</p><p>相关环境变量则有<code>C_INCLUDE_PATH</code>和<code>CPLUS_INCLUDE_PATH</code>,也可以通过配置这两个变量新增头文件搜索路径,使得C和C++源程序中能够找到其<code>include</code>的头文件。</p><p>注:用<strong>GCC</strong>参数选项指定的目录会在由环境变量指定的目录之前被优先搜索。什么参数都不指定的时候,<strong>GCC</strong>会优先搜索当前工作目录,以查找头文件和库。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><hr><p>[1]<a href="https://blog.csdn.net/yuleslie/article/details/7695102" target="_blank" rel="noopener">Linux 动态库与静态库</a><br>[2]<a href="https://typecodes.com/cseries/gcclderrlibrarypath.html" target="_blank" rel="noopener">LIBRARY_PATH与LD_LIBRARY_PATH的区别</a><br>[3]<a href="http://man7.org/linux/man-pages/man8/ld.so.8.html" target="_blank" rel="noopener">ld Man Page</a><br>[4]<a href="https://gcc.gnu.org/onlinedocs/gcc/Environment-Variables.html" target="_blank" rel="noopener">GCC Environment Variables</a><br>[5]<a href="https://stackoverflow.com/questions/4250624/ld-library-path-vs-library-path" target="_blank" rel="noopener">LD_LIBRARY_PATH VS LIBRARY_PATH - StackOverFlow</a><br>[6]<a href="https://blog.csdn.net/mybelief321/article/details/9099659" target="_blank" rel="noopener">GCC指定头文件和库文件搜索路径</a><br>[7]<a href="https://blog.csdn.net/blognkliming/article/details/45980391" target="_blank" rel="noopener">C++动态库与静态库</a></p>]]></content>
<categories>
<category> Development </category>
</categories>
<tags>
<tag> Linux </tag>
<tag> C/C++ </tag>
<tag> GCC </tag>
<tag> Environment Variable </tag>
</tags>
</entry>
<entry>
<title>TCP分段与IP分片的区别与联系</title>
<link href="/2018/06/01/Network/Mechanism/segmentation-fragmentation/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><hr><p>我们在学习TCP/IP协议时都知道,TCP报文段如果很长的话,会在发送时发生<strong>分段(Segmentation)</strong>,在接收时进行重组,同样IP数据报在长度超过一定值时也会发生<strong>分片(Fragmentation)</strong>,在接收端再将分片重组。如果之前你和曾经的我一样,经常混淆这两个概念,甚至一度以为两者表示的是同一个协议栈机制,那么本文就将通过详细介绍分段与分片的区别与联系,力图让你对此有一个更为完整、严谨的理解。<br><a id="more"></a></p><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><hr><p>首先需要强调的一点是,不管你之前从什么渠道获取了关于分段/分片方面的知识,甚至会觉得两者只是叫法不同但实际意思相同,但在本文中,</p><blockquote><p><strong>分段特指发生在使用TCP协议的传输层中的数据切分行为</strong><br><strong>分片特指发生在使用IPv4协议的网络IP层中的数据切分行为</strong></p></blockquote><p>这一点在文章标题中已经表达得十分明显。</p><p>正如上文所述,分段与分片发生在协议栈的不同层次,但目的都一样:</p><blockquote><p>都是为了能够传输上层交付的、数据量超过本层传输能力上限的数据,<strong>不得已</strong>才做的数据切分。</p></blockquote><p>注意到这里我用了修饰词,“不得已” — 也就是说在正常情况尽量避免做数据切分,能不分就不分,而只有在特殊场景下才不得不这么做。</p><p>这么形容显然是有原因的,最起码有以下两点解释:</p><ol><li>数据切分对于协议栈而言,显然使处理逻辑变得更加复杂了,在发送端需要做切分,甚至在路径中的转发设备中也需要切分(后文会介绍这种情况),在接收端又要做重组,处理开销明显增大,对设备的处理能力提出更高的要求;</li><li>在切分过程过不可避免的要为每个数据分片增加必要的协议首部以完成网络传输,在首部中还需要携带必要的顺序、偏移、是否属于同一块大数据等元信息来帮助组装。<br><br></li></ol><p>上述额外的空间开销在没有分片的情况下,显然是不需要的,发一块收一块即可,为了传输该数据包仅为其添加协议栈各层首部各一份,更无须设置用于组装的各种元数据,不论是发送还是接收的处理逻辑都更加简单。所以,能不切分就不切分,毫无疑问是正确的。</p><p>到此为止,关于数据切分技术出现的背景,包括其必要性和弊端都应该算是比较清晰了。</p><p>回到文章主题,前文有提到分段和分片工作在不同协议层,这其实很容易会造成一些疑惑,这些疑惑也一度伴随着我,比如:</p><ul><li>分段和分片有没有可能同时发生?为什么可能/不能?</li><li>如果可能,什么场景下会同时发生?</li><li>如果不能,什么场景下会发生分段?什么场景下又会发生分片?</li><li>分段和分片的切分与组装的过程是怎么样的?</li></ul><h2 id="概念"><a href="#概念" class="headerlink" title="概念"></a>概念</h2><hr><p>要回答以上问题,首先要介绍两个概念:</p><h3 id="最大传输单元-Maximum-Transmission-Unit"><a href="#最大传输单元-Maximum-Transmission-Unit" class="headerlink" title="最大传输单元(Maximum Transmission Unit)"></a>最大传输单元(Maximum Transmission Unit)</h3><hr><p>最大传输单元(Maximum Transmission Unit),即<strong>MTU</strong>,为数据链路层的最大载荷上限(即IP数据报最大长度),每段链路的MTU可能都不相同,一条端到端路径的MTU由这条路径上MTU最小的那段链路的MTU决定。</p><p>MTU是链路层中的网络对数据帧的一个限制,以以太网为例,MTU通常为1500字节,采用巨帧(Jumbo Frame)时可以达到9000字节。所谓的MTU,是二层协议的一个限制,对不同的二层协议可能有不同的值,只有二层协议为以太网(<strong>Ethernet</strong>)时,MTU一般才取1500字节,注意它不是物理链路介质的限制,只有工作在二层的设备才需要指定MTU的值,如网卡、转发设备端口(统称为<strong>网络接口</strong>)等,通过同一段线缆直连的通信端口或网卡,其MTU值一定相同。</p><p>一个IP数据报在以太网中传输,如果它的长度大于当前链路MTU值,就要进行<strong>分片传输(这里指IP层分片)</strong>,使得每片数据报的长度都不超过MTU。分片传输的IP数据报不一定按序到达,但IP首部中的信息能让这些数据报片按序组装。IP数据报的分片与重组是在网络IP层完成的。</p><h3 id="最大报文段长度-Maximum-Segment-Size"><a href="#最大报文段长度-Maximum-Segment-Size" class="headerlink" title="最大报文段长度(Maximum Segment Size)"></a>最大报文段长度(Maximum Segment Size)</h3><hr><p>最大报文段长度(Maximum Segment Size),即<strong>MSS</strong>,为TCP传输层的最大载荷上限(即应用层数据最大长度),TCP三次握手期间通过TCP首部选项中的MSS字段通知对端,通常一条TCP连接的MSS取通信双方较小的那一个MSS值,与MTU的换算关系为:</p><p><center><strong>MTU = MSS + TCP首部长度 + IP首部长度</strong></center><br></p><p>故在以太网中(网络层以IPv4为例):</p><p><center><strong>MSS = 以太网MTU - TCP首部长度 - IPv4首部长度 = 1500 - 20 - 20 = 1460字节</strong></center><br></p><p>未指定MSS时默认值为536字节,这是因为在Internet中标准的MTU值为576字节,576字节MTU = TCP首部长度20字节 + IPv4首部长度20字节 + 536字节MSS。</p><p>一个应用程序如果要发送超过MSS大小的数据,就要进行<strong>分段传输(这里指TCP分段)</strong>,使得每个报文段长度都不超过MSS。分片传输的TCP报文段不一定按序到达,但实现可靠传输的TCP协议中有处理乱序的机制,即利用报文段序列号在接收缓冲区进行数据重排以实现重组。TCP分段的重组是在TCP传输层完成的。</p><h2 id="区别"><a href="#区别" class="headerlink" title="区别"></a>区别</h2><hr><p>有了前文的知识准备,不难得出结论:</p><blockquote><p><strong>TCP分段的原因是因为TCP报文段大小受MSS限制</strong><br><strong>IP分片的原因则是因为IP数据报大小受MTU限制</strong></p></blockquote><p>由于一直有 $MSS \leq MTU$ 成立,很明显,分段后的每一个TCP报文段再加上IP首部后的长度构造成的IP数据报长度都不可能超过MTU,因此也就不需要在网络层进行IP分片了。</p><p>相比于为实现面向连接、提供可靠传输服务而具备各种复杂机制的TCP,UDP是简单至极的传输协议,就是在IP协议基础上增加了些无关痛痒的特性以实现端到端的数据传输服务,因此UDP提供的是类似IP的“尽力而为”的传输服务,是不可靠的。而由于UDP协议并不会自行分段,故MSS的限制对其没有作用,因此最终的IP数据报的长度超过了MTU时,网络层会负责执行IP分片。同样,(没有分段功能的)ICMP数据在网络层中同样会出现IP分片的情况。</p><p>简而言之:</p><blockquote><p>UDP不会分段,就由IP来分片。TCP会分段,当然就不用IP来分了!</p></blockquote><p>另外一个值得注意的是,在分片的数据中,传输层的首部只会出现在第一个分片中,IP数据报分片后,只有第一片带有传输层首部(UDP或ICMP等),后续分片只有IP首部和应用数据,到了目的地后根据IP首部中的信息在网络层进行重组,这一步骤对上层透明,即传输层根本不知道IP层发生了分片与重组。而TCP报文段的每个分段中都有TCP首部,到了目的地后根据TCP首部的信息在传输层进行重组。</p><p>TCP分段仅发生在发送端,这是因为在传输过程中,TCP分段是先被封装成IP数据报,再封装在以太网帧中被链路所传输的,并且在端到端路径上通常不会有工作在三层以上,即传输层的设备,故TCP分段不会发生在传输路径中间的某个设备中,在发送端TCP传输层分段后,在接收端TCP传输层重组。</p><p>IP分片不仅会发生在在使用UDP、ICMP等没有分段功能的传输层协议的数据发送方,更还会发生在传输途中,甚至有可能都会发生,这是因为原本的大数据报被分片后很可能会经过不同MTU大小的链路,一旦链路MTU大于当前IP分片大小,则需要在当前转发设备(如路由器)中再次分片,但是各个分片只有到达目的地后才会在其网络层重组,而不是像其他网络协议,在下一跳就要进行重组。</p><p>所以有一点需要特别强调:</p><blockquote><p>发送端进行TCP分段后就一定不会在IP层进行分片,因为MSS本身就是基于MTU推导而来,TCP层分段满足了MSS限制,也就满足了MTU的物理限制。但在TCP分段发生后仍然可能发生IP分片,这是因为TCP分段仅满足了通信两端的MTU要求,传输路径上如经过MTU值比该MTU值更小的链路,那么在转发分片到该条链路的设备中仍会以更小的MTU值作为依据再次分片。当然如果两个通信主机直连,那么TCP连接协商得到的MTU值(两者网卡MTU较小值)就是端到端的路径MTU值,故发送端只要做了TCP分段,则在整个通信过程中一定不会发生IP分片。</p></blockquote><p>特别地,对中途发生分片的数据报而言,即使只丢失其中一片数据也要重传整个数据报(这里既然有重传,说明传输层使用的是具有重传功能的协议,如TCP协议。而UDP协议不可靠,即使丢包了也不在意更不会重传,所以必须在应用层实现可靠通信的逻辑),这是因为IP本身没有重传机制,只能由更高层,比如传输层的TCP协议,来负责重传以确保可靠传输。于是,当来自同一个TCP报文段封装得到的原始IP数据报中的的某一片丢失后,接收端TCP迟迟接受不到完整报文段,它就会认为该报文段(包括全部IP分片)已丢失,TCP之后由于超时会收到三个冗余ACK就会重传整个TCP报文段,该报文段对应于一份IP数据报,可能有多个IP分片,但没有办法单独重传其中某一个数据分片,只能重传整个报文段。</p><h2 id="联系"><a href="#联系" class="headerlink" title="联系"></a>联系</h2><hr><p>分片或分段发生的根源都在于MTU这一数据链路层限制,由于更靠近数据链路层的IP层在感知MTU方面相比于传输层具备天然的优势,在大小超过MTU的大数据报传输问题出现伊始,IP层分片技术就成为主流解决方案。而分片带来的诸多开销(额外首部、复杂处理逻辑)以及其甚至可能在端到端的传输过程中多次发生在网络转发设备(路由器)的问题,让网络协议设计者们又要费尽心思地在端到端通信过程中避免IP分片。</p><p>TCP分段技术被提出后,在一定程度上减少了IP分片,但正如上一节末尾所言,TCP分段仅是在发送端避免了IP分片,但是却不能保证在整个端到端通信路径上不会发生IP分片,因为路径上经常会有MTU值小于该TCP连接协商得到的MTU值的链路,在转发至该段链路之前转发设备仍需分片,所以说TCP分段并不能完全避免IP分片。</p><p>那么如何才能彻底避免分片呢?</p><p>答案其实不难想到:如果能在TCP连接双方正式通信之前,就能通过某种方式预先知道端到端路径的MTU,即路径中包含的各条链路的MTU最小值(称为路径MTU,<strong>Path MTU</strong>),这一预先获知路径MTU的过程,称为路径MTU发现(<strong>Path MTU Discovery</strong>),这样此后每次通信都会基于此MTU推导得到的MSS值在发送方TCP层处执行分段。</p><p>路径MTU发现如何实现呢?</p><p>大家都记得IP首部中有三个标志位,第一位预留,第二位<strong>DF(Don’t Fragment)</strong>,第三位<strong>MF(More Fragments)</strong>。其中<strong>DF</strong>如果为1,意思是这个IP数据报在传输的过程中不能分片,如果此IP数据报大于网络接口的MTU,请直接丢弃,并发送消息告诉源主机已丢包。什么消息呢?<strong>ICMP</strong>的消息,告诉包因为太大了,因为不能分片所以丢弃了,并告诉源主机请重新发送不超过MTU的数据报,那发什么样的ICMP消息呢?再回顾一下ICMP首部结构,有一个<strong>Type</strong>字段,还有<strong>Code</strong>字段,发送<strong>Type=3, Code=4, MTU=该接口的MTU值X</strong>的消息既可以了。当这个ICMP消息到达IP数据报的源主机,源主机知道原来是IP数据报太大了,那最大可以发送多大的包呢?ICMP消息里有,那就是MTU=丢弃该包的网络接口的MTU值X,于是源主机再次发送不超过MTU=X的数据报就可以避免在传输路径上的IP分片。</p><p>有人会说,如果这个大小不超过X的IP数据报在传输过程中又遇到更小的MTU怎么办?重复以上步骤即可~</p><p>路径MTU发现看似完美避免了IP分片的问题,但同时又带来了新的问题:如果ICMP消息最终没能到达源主机怎么办?很显然该IP数据报就被静静丢弃了,TCP连接超时而被断开。ICMP为什么回不来?一般是被防火墙或路由器的访问控制列表(Access Control List, ACL)给无情拒绝了。如果你可以管理并配置这些设备,只要允许ICMP <strong>Type=3, Code=4</strong> 的消息可以通过即可,否则只有老老实实关闭路径MTU发现功能了,因为至少分片还能通信,而避免分片则彻底无法通信了…</p><h2 id="实例"><a href="#实例" class="headerlink" title="实例"></a>实例</h2><hr><p>参见<a href="https://blog.csdn.net/yusiguyuan/article/details/22782943" target="_blank" rel="noopener"><strong>2</strong></a>与<a href="http://www.cnblogs.com/diegodu/p/4647644.html" target="_blank" rel="noopener"><strong>3</strong></a></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><hr><ol><li>(TCP)分段和(IP)分片各自发生在不同的协议层(分段-TCP传输层,分片-IP层)</li><li>切分的原因不尽相同(数据量超出上限,分段应用数据上限-MSS,分片上限-MTU)</li><li>虽然分段和分片不会在发送方同时发生,但却可能在同一次通信过程中分别在发送主机(分段)和转发设备(分片)中发生</li><li>发送端采用的传输层协议不同(分段-TCP,分片-UDP/ICMP…)</li><li>分段仅可能发生在发送端,分片不仅可能发生在发送端,更还可能发生在路径上任何一台工作在三层或以上的设备中,而两者的重组都只会发生在接收端</li></ol><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><hr><p>1.<a href="https://blog.csdn.net/ns_code/article/details/30109789" target="_blank" rel="noopener">网络协议-TCP分段与IP分片</a><br>2.<a href="https://blog.csdn.net/yusiguyuan/article/details/22782943" target="_blank" rel="noopener">TCP层分段与IP层分片的关系</a><br>3.<a href="http://www.cnblogs.com/diegodu/p/4647644.html" target="_blank" rel="noopener">IP分片浅析</a><br>4.<a href="http://www.cnblogs.com/lshs/p/6038494.html" target="_blank" rel="noopener">TCP常见选项</a><br>5.<a href="https://www.zhihu.com/question/22181709" target="_blank" rel="noopener">TCP/IP协议栈中为什么选择IP层负责分片</a><br>6.<a href="https://blog.csdn.net/stpeace/article/details/74152332" target="_blank" rel="noopener">为什么IP层要分片而TCP层要分段?</a><br>7.<a href="https://www.cnblogs.com/wilber2013/p/4853674.html" target="_blank" rel="noopener">动手学习TCP-数据传输</a></p>]]></content>
<categories>
<category> Network </category>
</categories>
<tags>
<tag> Segmentation </tag>
<tag> TCP </tag>
<tag> Fragmentation </tag>
<tag> IP </tag>
</tags>
</entry>
<entry>
<title>网络测量之EverFlow(SIGCOMM-2015)</title>
<link href="/2018/05/25/Network/Measurement/related-work-everflow/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><p><a href="https://conferences.sigcomm.org/sigcomm/2015/" target="_blank" rel="noopener"><strong>SIGCOMM 2015</strong></a>年中,由微软研究院发表了题为《Packet-Level Telemetry in Large Datacenter Networks》的论文。基于本文设计的<strong>EverFlow</strong>系统,运行在端系统之上,并利用已有的<strong>OpenFlow</strong>交换机的功能特性,实现了数据中心网络中<strong>Packet-Level</strong>的网络数据监测。本文尝试概述了该系统的核心思想,作为网络测量领域相关工作的一次备忘。<br><a id="more"></a></p><h2 id="Paper"><a href="#Paper" class="headerlink" title="Paper"></a>Paper</h2><hr><p>Proposed in <a href="https://conferences.sigcomm.org/sigcomm/2015/" target="_blank" rel="noopener">SIGCOMM.2015</a><br>-<a href="https://conferences.sigcomm.org/sigcomm/2015/pdf/papers/p479.pdf" target="_blank" rel="noopener"><strong>《Packet-Level Telemetry in Large Datacenter Networks》</strong></a></p><h2 id="Background"><a href="#Background" class="headerlink" title="Background"></a>Background</h2><hr><p>调试复杂网络环境通常要求捕获并分析数据包级别(<strong>packet-level</strong>)粒度的流量数据,而数据中心网络以其规模(节点数)多、流量多和故障种类多样,对这一问题带来了更严峻的挑战。为了及时检测出网络故障,数据中心网络运维人员必须做到如下几点:</p><ol><li><strong>Identify affected packets inside large volume of traffic.</strong> 即在大规模流量中识别与故障相关的流量</li><li><strong>Track them across multiple network components.</strong> 即在多个网络设备中追踪这些问题流量</li><li><strong>Analyze traffic traces for fault patterns.</strong> 即通过分析流量轨迹发现故障模式</li><li><strong>Test or confirm potential causes.</strong> 即通过测试确认造成故障的潜在原因</li></ol><h2 id="Idea"><a href="#Idea" class="headerlink" title="Idea"></a>Idea</h2><hr><p>本文提出的<strong>EverFlow</strong>系统,采用<strong>“match and mirror”</strong>,即匹配后再做镜像的方法,来一致性地追踪网络中的单个数据包,一致性指的是在每个交换机处都追踪到同一个数据包,对应配置相同的匹配规则即可,这是商用交换机已经具备的特性,即针对匹配特定模式(数据包头部或载荷)的数据包采取特定的动作(Action),这里的动作可设置为镜像即复制并发送数据包到分析服务器处。通过安装较小数目的精心选择的匹配-镜像规则,意味着可以将数据包追踪造成的额外流量带宽开销减小几个数量级,因为我们仅仅追踪那些与规则相匹配的数据包而非全部。除了以上提到的的“选择性镜像”功能,<strong>EverFlow</strong>还提供一种叫作<strong>Guided Probe</strong>,即向导指针,是一种专门制作出并被注入到网络中的数据包,用于追踪之前预设的路径,相当于流量重放(<strong>Replay</strong>),它能够帮助测试与验证单个设备或链路的性能和行为。</p><h2 id="Challenge"><a href="#Challenge" class="headerlink" title="Challenge"></a>Challenge</h2><hr><ol><li><strong>Tracing and analysis scalability</strong>,追踪与分析数据包的扩展性问题</li><li><strong>Limitations of passive tracing</strong>,即被动测量方法的局限性</li></ol><p>数据中心网络中传输空前巨大的流量数据,对于商用交换机而言,分析其中一个很小的数据包子集都很难做到,而将数据包数据发送至商用分析服务器又会造成拥塞甚至瘫痪整个网络,另外,即使是使用能够线速(<strong>line rate,假设为10Gbps</strong>)处理轨迹数据的服务器,对于如此海量的数据仍然意味着数目众多的服务器带来的成本开销(考虑负载均衡和局部性的话可能需要的服务器数目更多)。并且,数据中心网络中的故障通常发生在多跳或多个交换机上,而要做到高效的故障诊断则需要能够在网络中智能地追踪这部分故障流量的数据包,即需要能够根据复杂的查询模式,比如匹配协议头部、源和目的地址,甚至路径上的特定设备,来搜索数据包的轨迹数据。</p><p>而已有的网络测量工作,通常采用被动追踪的方式,只能捕获到网络的一个瞬时快照,效果有限。因为瞬时快照中显示的轨迹信息不足以判断问题是持久性或是暂时性的,并可能无法提供足够的信息用于定位故障发生的位置,当然也足以确定故障发生的根本原因。</p><h2 id="Solution"><a href="#Solution" class="headerlink" title="Solution"></a>Solution</h2><hr><p>前三个用于解决<strong>Challenge</strong>中的可扩展性问题,最后一个则克服了被动测量的局限性。</p><h3 id="Match-and-Mirror-on-Switch"><a href="#Match-and-Mirror-on-Switch" class="headerlink" title="Match and Mirror on Switch"></a>Match and Mirror on Switch</h3><hr><p><strong>EverFlow</strong>基于商用交换机已有的<strong>“match-and-acton”</strong>的特性,在不改变转发行为的情况下,设计了以下三种类型的匹配规则,大大减小了数据包追踪(带宽)开销:</p><h4 id="TCP-Flow-Tracing-Rules"><a href="#TCP-Flow-Tracing-Rules" class="headerlink" title="TCP Flow Tracing Rules"></a>TCP Flow Tracing Rules</h4><p>由于数据中心网络中,TCP流量占绝大多数,该规则基于TCP首部的<strong>SYN</strong>、<strong>FIN</strong>和<strong>RST</strong>字段来匹配全部的TCP数据包。</p><h4 id="Debug-Bit-Tracing-Rules"><a href="#Debug-Bit-Tracing-Rules" class="headerlink" title="Debug-Bit Tracing Rules"></a>Debug-Bit Tracing Rules</h4><p>除了追踪全部TCP流的数据包,还需要一种更灵活的匹配规则,用于追踪运维人员更加关注的数据包,比如特定应用相关的流量(带有特定端口号),或特定源和目的服务器之间的流量等等。这可通过在主机端对数据包头部打上特殊标记位,<strong>EverFlow</strong>中称为<strong>Debug Bit</strong>,并为交换机配置匹配该标记的数据包的规则,应用开发者在其编写的监控应用程序中对包进行标记,就可以追踪到任何他们感兴趣的普通数据流量。</p><h4 id="Special-Protocol-Tracing-Rules"><a href="#Special-Protocol-Tracing-Rules" class="headerlink" title="Special Protocol Tracing Rules"></a>Special Protocol Tracing Rules</h4><p>在数据中心网络中,除了普通数据流量之外,还有少量的特殊协议流量,如<strong>BGP</strong>、<strong>PFC</strong>和<strong>RDMA</strong>等,尽管它们的数据量很小,但是对于数据中心网路的整体性能及健康运行有关键影响。<strong>EverFlow</strong>也为协议流量设计了专门的匹配规则。</p><h3 id="Scalable-Trace-Analyzers"><a href="#Scalable-Trace-Analyzers" class="headerlink" title="Scalable Trace Analyzers"></a>Scalable Trace Analyzers</h3><hr><p>尽管匹配规则的使用已经大大减小了数据包追踪开销,但是由于数据中心网络流量的巨大规模,这些由于追踪而产生的额外流量仍然是海量的,这为轨迹数据的分析带来了巨大挑战。为减小分析开销,注意到在任意时刻,仅仅很小一部分被追踪的数据包会表现出异常行为,论文中给出的数据是<strong>0.01%</strong>,如出现路由环路和丢包等。基于这个现象,<strong>EverFlow</strong>将异常数据包轨迹与正常数据包轨迹数据进行区别对待,即对于异常数据包轨迹数据,保存其详细的、每跳的状态,而对于正常数据包轨迹数据,则仅保存聚合的、每个设备的状态,且在必要的时候,我们能够通过<strong>Guided Probe</strong>有选择性地从正常数据包轨迹数据中恢复信息。</p><h3 id="Switch-based-Reshufflers"><a href="#Switch-based-Reshufflers" class="headerlink" title="Switch-based Reshufflers"></a>Switch-based Reshufflers</h3><hr><p>利用已有的将商用交换机转换成为硬件实现的数据选择器(<strong>Hardware Mux(Multiplexer)</strong>)的工作,实现轨迹数据处理的负载均衡。其主要思想为:首先为一个交换机,即一个HMux赋予一个VIP(<strong>Virtual IP</strong>),并将该VIP映射到一组DIP(<strong>Direct IP</strong>),而每个DIP又与一个分析服务器对应。<strong>EverFlow</strong>中配置所有的交换机都将其所匹配并追踪的数据包发送至该VIP处。当一个被追踪的数据包到达该HMux,HMux会基于该数据包的五元组(又称Flow ID,即源/目的IP地址、源/目的端口号和协议号)作哈希,根据哈希值将该数据包转发至相应的DIP处,这也保证了具有相同五元组的被追踪的数据包会被转发至同一个DIP,即同一个流的数据包会被同一个分析服务器所处理。</p><p>HMux能够利用交换机全部的转发能力(通常大于1 Tbps)来对流量进行“洗牌(<strong>Shuffle</strong>)”,这比用于实现负载均衡的服务器的网卡(通常10 Gbps)转发速度至少快了100倍,并还能够通过配置多个具有相同VIP的HMux来进一步增强<strong>Shuffle</strong>的能力。</p><h3 id="Guided-Probing"><a href="#Guided-Probing" class="headerlink" title="Guided Probing"></a>Guided Probing</h3><hr><p>之前有提到说,仅采用被动测量可能不足以区分导致故障(如丢包)发生的多种可能性,现在考虑这样一个问题:<strong>如果</strong>我们能够做到任意地重放(<strong>Replay</strong>)一个数据包的轨迹呢?更具体而言,<strong>如果</strong>我们能将某个特定的数据包注入到网络中,使其经过特定的交换机,并且通过为其设置”Debug Bit”以追踪该数据包的轨迹和行为呢?</p><p>如果能做到上述“如果”,现有问题会变得简单许多,基于这个思想,<strong>EverFlow</strong>引入了<strong>Guided Probe(向导探针)</strong>,本质是一个精心制作(<strong>Specially Crafted</strong>)的数据包,属性与需要重放的数据包一样,即原数据包的替身(<strong>Incarnation</strong>),用于检验原数据包在网络中的行为。<strong>EverFlow</strong>中进一步扩展<strong>Guided Probe</strong>的功能,使其不仅能够将某个数据包注入特定的交换机,而且还可以强制该数据包经过指定的一个跳序列,类似源端路由(<strong>Source Routing</strong>)的感觉,并且通过封装(<strong>Encapsulation</strong>)的方法使得该数据包会在注入某个交换机后遵循某个预先设置好的路径。</p><p><strong>Guided Probe</strong>一个很直接的用途在于恢复由于采样或聚合而丢失的轨迹信息、为了恢复任意数据包的轨迹,我们只需简单地将该数据包(预先标记<strong>“Debug Bit”</strong>)重新注入其第一跳交换机中即可。此外,<strong>Guided Probe</strong>还可用于克服被动测量方法的限制,比如判断某次丢包是否是持久性的,而持久性故障又是应该优先解决的,我们可以通过制作多个不同模式(如五元组)的探针数据包注入网络,来观察丢包是随机的,还是针对特定的五元组。</p><h2 id="Application"><a href="#Application" class="headerlink" title="Application"></a>Application</h2><hr><p>文中还提到,针对不同测量场景或需求,基于<strong>EverFlow</strong>还设计了以下四种应用程序帮助网络故障的调试。</p><h3 id="Latency-Profiler"><a href="#Latency-Profiler" class="headerlink" title="Latency Profiler"></a>Latency Profiler</h3><hr><p><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/Measurement/related-work-everflow/measuring-link-latency.png" alt="image"></p><p>许多数据中心网络服务,诸如搜索和分布式缓存都要求低延迟响应。应用<strong>Latency Profiler</strong>专门用于分析某对通信结点之间的通信延迟过高的问题。</p><p><strong>Latency Profiler</strong>会在源主机端标记该TCP连接的SYN数据包的<strong>Debug Bit</strong>,用于在全网范围内追踪该数据包的轨迹,从而可以获知其路径上经过的每个网络设备信息,之后就通过<strong>Controller</strong>提供的<strong>Probe()</strong>的API向每个网络设备注入探针,从探针包的轨迹信息中可计算出每跳的延迟(<strong>Per-Hop Latency</strong>)。对于端主机间延迟较高的问题,<strong>Latency Profiler</strong>利用类似“源路由”的方法,如上图(b)所示,发射预先指定路径为“S1->S2->S1”的探针,此时测量到的延迟数据实际是链路(S1,S2)的<strong>RTT</strong>(<strong>Round-trip Time</strong>),即往返时延,而非单向链路延迟,如图(a)中所示,如果仅仅以该数据包从S1发出的镜像与从S2发出的镜像到达分析服务器的时间差作为链路(S1,S2)的延迟,一个很明显的缺陷在于从S1到达分析服务器的路径 <strong>Path 1</strong>,以及从S1经过S2再到达分析服务器的路径 <strong>Path 2</strong> 可能大相径庭,从而这两条路径中后者仅比前者多一条链路(S1,S2)的理想情况可能与现实相差甚远,测量结果更难言准确。</p><h3 id="Packet-Drop-Debugger"><a href="#Packet-Drop-Debugger" class="headerlink" title="Packet Drop Debugger"></a>Packet Drop Debugger</h3><hr><p><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/Measurement/related-work-everflow/debugging-packet-drops.png" alt="image"></p><p>丢包会严重降低应用性能,进而导致吞吐量低、超时甚至不可达等故障。丢包问题难以调试的原因在于产生丢包的可能原因有很多,如拥塞、软件错误或配置错误等。应用<strong>Packet Drop Debugger</strong>专门用于分析网络中的丢包问题。</p><p><strong>Packet Drop Debugger</strong>会去检查判断为已丢失的数据包的轨迹,找到其轨迹显示的最后一跳 $S_n$,即该数据包最后被捕获的位置,可根据该跳的转发表中对应表项对应的输出端口来推断其下一跳,或者直接根据数据中心拓扑信息与路由策略来推断其下一跳。之后<strong>Packet Drop Debugger</strong>向 $S_n$ 注入探针,以确定该丢包是持续性的(重要,必须解决)还是暂时性的(不那么重要,甚至可忽略,大多数时候任其自行修复),如果是持续性的则还需要判断该丢包是随机的(端口故障)还是具有某特定模式(如匹配固定五元组,可能因为路由表对应表项崩了)。</p><h3 id="Loop-Debugger"><a href="#Loop-Debugger" class="headerlink" title="Loop Debugger"></a>Loop Debugger</h3><hr><p>路由环路在数据中心网络中实际上并不常见,可一旦出现环路仍会导致不必要的资源浪费或连通性问题。应用<strong>Loop Debugger</strong>专门用于分析路由环路问题。</p><p><strong>Loop Debugger</strong>会去检查判断为出现环路的数据包的轨迹,并向网络中注入探针以检测环路是否持续存在,如果环路仍旧存在那么它就会向运维人员发送一份环路所涉及设备的清单,后者可以简单地关闭一些设备接口以打破环路。应用会继续发送探针,直至环路消失。</p><h3 id="ECMP-Profiler"><a href="#ECMP-Profiler" class="headerlink" title="ECMP Profiler"></a>ECMP Profiler</h3><hr><p>在数据中心网络中,交换机通常使用<a href="https://en.wikipedia.org/wiki/Equal-cost_multi-path_routing" target="_blank" rel="noopener"><strong>ECMP</strong></a>。由于糟糕的哈希函数或路由问题,负载的分配可能会非常不均衡,从而导致链路出现拥塞。应用<strong>ECMP Profiler</strong>专门用于分析负载均衡问题。</p><p>针对每个交换机,<strong>ECMP Profiler</strong>会监控每条链路的聚合流量。一旦检测到不均衡的负载分配,它就会通过访问更细粒度的负载计数器以判断不公平的负载分配是影响到了全部的流量,还是总流量的子集(如从某固定前缀网络发出或去往固定前缀网络)。</p><h3 id="RoCEv2-based-RDMA-Debugger"><a href="#RoCEv2-based-RDMA-Debugger" class="headerlink" title="RoCEv2-based RDMA Debugger"></a>RoCEv2-based RDMA Debugger</h3><hr><p>基于<a href="https://en.wikipedia.org/wiki/RDMA_over_Converged_Ethernet" target="_blank" rel="noopener"><strong>RoCEv2</strong></a>的<a href="https://en.wikipedia.org/wiki/Remote_direct_memory_access" target="_blank" rel="noopener"><strong>RDMA</strong></a>是一种新兴网络协议,可在低CPU开销情况下实现高吞吐量(40 Gbps)和极低时延(毫秒级)。通过采用<a href=""><strong>PFC</strong></a>来实现无丢包的以太网架构,<strong>RDMA</strong>协议的实现可被进一步简化并<strong>Offload</strong>到网卡上去。然而在文中提到的微软的数据中心网络中,由于网卡中的软件错误,<strong>RDMA</strong>经常难以获得其最理想的性能。而调试此类问题的困难在于网卡是由第三方厂商制造,调试网卡中的<strong>RDMA</strong>代码的工具十分有限。作者为此设计了应用<strong>RoCEv2-based RDMA Debugger</strong>专门用于分析<strong>RDMA</strong>流量问题。</p><p><strong>RoCEv2-based RDMA Debugger</strong>会追踪所有<strong>RDMA</strong>相关的控制数据包,如<strong>PFC</strong>和<strong>NACK</strong>,这些控制报文的轨迹就提供了一个可靠且相对独立的方式使我们不仅能够观察到<strong>RDMA</strong>网络流的真实行为,也有助于调试第三方厂商在网卡中的实现代码。</p><h2 id="Achitecture"><a href="#Achitecture" class="headerlink" title="Achitecture"></a>Achitecture</h2><hr><p><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/Measurement/related-work-everflow/architecture.png" alt="image"></p><p>如上图所示,<strong>EverFlow</strong>中包含四个核心组件:<strong>Controller</strong>、<strong>Analyzer</strong>、<strong>Storage</strong>和<strong>Reshuffler</strong>。在这些组件之上,还编写了一系列利用<strong>EverFlow</strong>提供的数据包级别信息来检测网络故障的应用程序,下一节会重点介绍。</p><p>在<strong>EverFlow</strong>中,<strong>Controller</strong>会与应用程序交互,并协调各个组件的工作。在初始化阶段,它将相关规则配置在交换机上,而匹配这些规则的数据包会被发送至<strong>Reshuffler</strong>处,然后被转发至<strong>Analyzer</strong>,并输出分析结果存储在本地。<strong>Controller</strong>还提供了API接口,供给上层应用程序查询分析结果、定制<strong>Analyzer</strong>上的计数器、注入<strong>Guided Probe</strong>以及在主机端标记<strong>Debug Bit</strong>。</p><h3 id="Analyzer"><a href="#Analyzer" class="headerlink" title="Analyzer"></a>Analyzer</h3><hr><p><strong>Analyzers</strong>是一个分布式服务器集群,每个服务器处理一部分被追踪的流量。<strong>Reshuffler</strong>会实现这些流量在<strong>Analyzer</strong>服务器之间的负载均衡,并保证同一个流(具有相同五元组)的数据包会被转发至同一个<strong>Analyzer</strong>分析处理。在每个<strong>Analyzer</strong>处,都会保持两种类型的“状态”:<strong>数据包轨迹</strong>和<strong>计数器</strong>。</p><h4 id="Trace"><a href="#Trace" class="headerlink" title="Trace"></a>Trace</h4><p><strong>Analyzer</strong>会保持一个数据包轨迹的表,每个表项即为一个数据包的轨迹,每条轨迹是同一个原数据包的多个镜像实例的链表。每条轨迹由五元组和IP标识符(<strong>Identification</strong>)唯一标识。它包含一个完整数据包内容的拷贝,和一个每跳(<strong>Per-Hop</strong>)信息的集合,包括镜像该数据包的交换机的IP地址、时间戳、TTL、源MAC地址和DSCP/ECN字段。</p><p>对于每条完整的数据包轨迹(<strong>Trace</strong>),<strong>Analyzer</strong>检查两种类型的问题:环路(<strong>Loop</strong>)和丢包(<strong>Drop</strong>)。有环表现为同一个设备在一个轨迹中出现了多次。而丢包的检测,则可利用数据中心网络拓扑和路由策略预先计算出期待的最后一跳(<strong>Expected Last Hop</strong>),将其与数据包轨迹中包含的最后一跳进行比较,若不同则说明发生了丢包。</p><p>为减小存储开销,<strong>Analyzer</strong>仅会将行为异常的数据包的轨迹(如出现环路或丢包)、被标记有<strong>Debug Bit</strong>的数据包的轨迹以及特殊协议的流量数据包的轨迹写入存储中。而对于其余流量的轨迹信息(行为正常的流量,通常占绝大多数),<strong>Analyzer</strong>会将其聚合成各种类型的计数器(下一节列举的),并且周期性地(每隔10s)将这些计数器的值写入到存储中。在最后,<strong>Controller</strong>会将各个<strong>Analyzer</strong>的计数器值按类型合并。</p><h4 id="Counter"><a href="#Counter" class="headerlink" title="Counter"></a>Counter</h4><h5 id="Link-Load-Counter"><a href="#Link-Load-Counter" class="headerlink" title="Link Load Counter"></a>Link Load Counter</h5><p>对于每条链路,<strong>Analyzer</strong>会基于数据包轨迹信息计算聚合负载,如流数目、数据包数目和数据量字节大小等。除此以外,它还能计算更细粒度的负载计数器,如由特定前缀的源发出的流量负载或数据中心内部流量负载,通过<strong>Controller</strong>所提供的相关API,<strong>EverFlow</strong>应用程序可以动态增加或删除这种粒度较细的负载计数器。</p><h5 id="Latency-Counter"><a href="#Latency-Counter" class="headerlink" title="Latency Counter"></a>Latency Counter</h5><p><strong>Analyzer</strong>会根据<strong>Guider Probe</strong>的轨迹信息计算每条链路的时延信息。</p><h5 id="Mirrored-Packet-Drop-Counter"><a href="#Mirrored-Packet-Drop-Counter" class="headerlink" title="Mirrored Packet Drop Counter"></a>Mirrored Packet Drop Counter</h5><p>镜像数据包在到达<strong>Analyzer</strong>的路上同样可能发生丢包,但是通常很好判断。根据事先计算好的路径与实际收集到的轨迹信息比较,信息缺失的那一跳就是发生丢包的位置,<strong>Analyzer</strong>同样会为这种情况维持一个计数器。</p><h3 id="Controller"><a href="#Controller" class="headerlink" title="Controller"></a>Controller</h3><hr><p><strong>EverFlow</strong>应用程序通过<strong>Controller</strong>提供的API与之交互,获取所需数据用于检测网络故障。通过以下API,应用程序可以查询数据包轨迹、安装细粒度负载计数器、发送<strong>Guided Probe</strong>,以及通过标记<strong>Debug Bit</strong>来选择性地追踪流量。</p><ul><li><strong>GetTrace</strong>(Filter, Condition, StartTime, EndTime)</li><li><strong>GetCounter</strong>(Name, StartTime, EndTime)</li><li><strong>AddCounter</strong>(Name, Filter) & RemoveCounter(Name)</li><li><strong>Probe</strong>(Format, Interval, Count)</li><li><strong>EnableDbg</strong>(Servers, Filter) & DisableDbg(Servers, Filter)</li></ul><h2 id="Implementation"><a href="#Implementation" class="headerlink" title="Implementation"></a>Implementation</h2><hr><p><strong>EverFlow</strong>系统的实现共计10000行C++代码,而前文提到的5个基于<strong>EverFlow</strong>的调试应用的实现共计700行C#代码。</p><h3 id="Switch-Configuration2"><a href="#Switch-Configuration2" class="headerlink" title="Switch Configuration2"></a>Switch Configuration2</h3><hr><p>默认首先配置各个交换机中<strong>TCAM</strong>表中用于匹配TCP流量中的SYN、FIN和RST等标志位被置位的数据包的规则。并使用<a href="https://en.wikipedia.org/wiki/Differentiated_services" target="_blank" rel="noopener"><strong>DSCP</strong></a>(<strong>Differential Service Code Point</strong>,差分服务代码点,共6个<strong>Bit</strong>)字段中的一个<strong>Bit</strong>作为<strong>Debug Bit</strong>,以及<a href="http://wiki.hping.org/9" target="_blank" rel="noopener"><strong>IPID</strong></a>(<strong>IP Packet Identification</strong>,IP数据报标识,用于标记同一个IP数据报的多个分片,共16个<strong>Bit</strong>)字段中的n个<strong>Bit</strong>用于从 $2^n$ 个数据包中采样到某一个,如配置使用<strong>IPID</strong>字段中的10位进行匹配的规则,就可以每1024个数据包中取样到其中1个。由于每台交换机都配置有相同的匹配规则,那么采样数据包的集合就会在所有的交换机上是一致的,即都会采样到相同的数据包。</p><p>对于封装过的数据包,如被<strong>Hux</strong>封装过的,配置规则使其按照其内层的<strong>TCP/IP</strong>头部进行匹配,使得该数据包会与其原来的未封装过的数据包(即内层数据包)会最终经过<strong>Shuffle</strong>被发往同一个分析服务器。</p><p>最后,还配置有用于匹配以太网类型为<strong>0x8808</strong>,即二层控制报文(包含<strong>PFC</strong>),TCP端口为179的(<strong>BGP</strong>)以及<strong>RDMA</strong>的<strong>NACK</strong>。</p><p>综上,在交换机中配置的匹配规则大约20条左右,仅仅占用了<strong>TCAM</strong>表中很小部分的内存空间。</p><p>当数据包匹配到上述任何一条规则,交换机就会将其镜像发送至<strong>ReShuffler</strong>,并且是以<strong>GRE(Generic Routing Encapsulation)</strong>格式封装,数据包格式如下图所示,可以看到有内外两层头部信息,其中外层源IP地址为镜像交换机的回环地址,目的IP地址为是<strong>Reshufflers</strong>的<strong>VIP</strong>,而外层数据包的载荷即为原数据包。在外层的<strong>GRE</strong>头部中有个协议字段,用于表示本数据包是<strong>EverFlow</strong>镜像数据包,而交换机会配置有一个黑名单以防止对镜像数据包再次镜像。</p><p><strong>“Match-and-Mirror”</strong>完全在交换机的数据平面实现,利用了交换机<strong>ASIC</strong>芯片强大的数据处理能力,并对交换机的CPU没有造成任何开销。</p><p><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/Measurement/related-work-everflow/mirrored-packet-format-gre.png" alt="image"></p><h3 id="Guided-Prober"><a href="#Guided-Prober" class="headerlink" title="Guided Prober"></a>Guided Prober</h3><hr><p><strong>Guided Prober</strong>的核心功能在于生成任意所需数据包并注入到指定目标交换机中。它使用 <strong>Raw Socket API</strong> 来修改任意的数据包字段,如IP和四层(TCP、UDP和ICMP等)等头部字段。探针的<strong>Debug Bit</strong>会被标记便于追踪,并且在其载荷中携带一个签名使其可被<strong>EverFlow Analyzers</strong>识别。</p><p>要将某个探针 $p$ 发送给某个交换机 $S$,利用了商用交换机普遍具备的解封装(<strong>Decapsulation</strong>)的能力。首先,将探针 $p$ 封装在 $p’$ 内部,并以交换机 $S$ 的环回地址作为其目的IP地址,之后将其发出。交换机 $S$ 接收到 $p’$ 之后执行解封装得到原数据包 $p$,然后就可以根据正常转发逻辑对其进行转发并观测其行为。</p><p>实际上,通过多次封装的方法还能使得某数据包流经固定的路径,这不难想到。</p><p>另外,为了防止探针数据包对于服务器应用程序的影响,故意将其TCP或UDP的校验和设置错误,这样探针会被目的主机丢弃。</p><h3 id="Analyzers"><a href="#Analyzers" class="headerlink" title="Analyzers"></a>Analyzers</h3><hr><p><strong>Analyzers</strong>使用一个定制的数据包捕获程序库来捕获镜像数据包,该库支持<a href="https://docs.microsoft.com/en-us/windows-hardware/drivers/network/ndis-receive-side-scaling2" target="_blank" rel="noopener"><strong>RSS(Receiver Side Scaling)</strong></a>,它允许<strong>Analyzer</strong>使用多个CPU核来接收数据包。该库根据源和目的IP的哈希结果来将数据包映射到某个CPU核,从而可以通过运行多个线程来最大化吞吐量。</p><h3 id="Storage"><a href="#Storage" class="headerlink" title="Storage"></a>Storage</h3><hr><p><strong>EverFlow</strong>的存储设计基于<a href="http://www.vldb.org/pvldb/1/1454166.pdf" target="_blank" rel="noopener"><strong>SCOPE</strong></a>来实现。在<strong>SCOPE</strong>中数据被组织为具有多个列的多行表格结构,<strong>EverFlow</strong>将数据包<strong>Trace</strong>存储在一个包含多个列的表中,这些列包含<strong>Trace</strong>三个方面的内容:① 完整的数据包内容,头部和载荷被存储在单独的列中以简化查询;② 每跳(<strong>Per-Hop</strong>)信息,如时间戳、TTL、源MAC地址和DSCP-ECN字段值,由于每个数据包流经的跳数是不同的,将其所有的每跳信息组合合并到一列中去;③ <strong>Trace</strong>的元数据,包括<strong>Trace</strong>长度,是否为探针,是否包含环路或丢包。这些<strong>Trace</strong>能通过<strong>Controller</strong>提供的上述API基于<strong>filter</strong>、<strong>Condition</strong>和时间范围(<strong>StartTime</strong>~<strong>EndTime</strong>)查询。<br>类似地,将来自所有的<strong>Analyzer</strong>的计数器<strong>Counter</strong>存储在一个表中,表中的每一行代表来自某个<strong>Analyzer</strong>的某个Counter的快照,除去Counter的名称(键Key)和数值(值Value),该行还包含<strong>Analyzer</strong>的ID以及快照对应时间戳。在响应计数器查询时,<strong>Controller</strong>会将匹配给定Counter名称和时间范围的全部行的Counter值进行求和之后再行返回。</p>]]></content>
<categories>
<category> Network </category>
</categories>
<tags>
<tag> Measurement </tag>
<tag> SIGCOMM </tag>
<tag> End-Host </tag>
<tag> Packet-Level </tag>
</tags>
</entry>
<entry>
<title>博客迁移声明</title>
<link href="/2018/05/24/Trivial/migration-to-tencent-cloud/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><p>我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:<br><a href="https://cloud.tencent.com/developer/support-plan?invite_code=3vxdxs0awkys0" target="_blank" rel="noopener">https://cloud.tencent.com/developer/support-plan?invite_code=3vxdxs0awkys0</a></p>]]></content>
<categories>
<category> Trivial </category>
</categories>
<tags>
<tag> Blog </tag>
<tag> Tencent Cloud </tag>
</tags>
</entry>
<entry>
<title>Linux内核调优参数对比与解释</title>
<link href="/2018/05/20/OS/Kernel/kernel-parameter-tuning/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><p>本文介绍了Linux系统性能优化点常见的内核参数含义及其调优方式,以供学习参考。<br><a id="more"></a></p><p>具体情况要具体分析,此处只是收集通常会利用到的一些参数的调整,做一些简单的对比和解释。<br>全部系统变量默认值测试均在 <strong>Ubuntu 16.03</strong> 系统下进行,内核版本为<strong>4.13.0-42-generic</strong>。</p><h2 id="Net"><a href="#Net" class="headerlink" title="Net"></a>Net</h2><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.ipv4.tcp_syncookies=1</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/ipv4/tcp_syncookies</span><br></pre></td></tr></table></figure></p><p>默认值:1<br>作用:是否打开SYN Cookie功能,该功能可以防止部分SYN攻击</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.ipv4.ip_local_port_range=1024 65535</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/ipv4/ip_local_port_range</span><br></pre></td></tr></table></figure></p><p>默认值:32768 60999<br>作用:可用端口的范围</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.ipv4.tcp_fin_timeout=30</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/ipv4/tcp_fin_timeout</span><br></pre></td></tr></table></figure></p><p>默认值:60(s)<br>作用:TCP挥手阶段FIN报文超时时间</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.ipv4.tcp_timestamps=1</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/ipv4/tcp_timestamps</span><br></pre></td></tr></table></figure></p><p>默认值:1<br>作用:TCP时间戳</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.ipv4.tcp_tw_recycle=0 # Ubuntu系统下未找到该变量</span><br></pre></td></tr></table></figure><p>查询命令:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/ipv4/tcp_tw_recycle</span><br></pre></td></tr></table></figure><p>默认值:0<br>作用:针对TIME-WAIT,不要开启。不少文章提到同时开启tcp_tw_recycle和tcp_tw_reuse,会带来C/S在NAT方面的异常。个人接受的做法是,开启tcp_tw_reuse,增加ip_local_port_range的范围,减小tcp_max_tw_buckets和tcp_fin_timeout的值(参考<a href="http://ju.outofmemory.cn/entry/91121" target="_blank" rel="noopener"><strong>这里</strong></a>和<a href="http://www.cnblogs.com/lulu/p/4149312.html" target="_blank" rel="noopener"><strong>这里</strong></a>)</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.ipv4.tcp_tw_reuse=1</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/ipv4/tcp_tw_reuse</span><br></pre></td></tr></table></figure></p><p>默认值:0<br>作用:针对TIME-WAIT,做为客户端可以启用(例如,作为nginx-proxy前端代理,要访问后端的服务)</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.ipv4.tcp_max_tw_buckets=262144</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/ipv4/tcp_max_tw_buckets</span><br></pre></td></tr></table></figure></p><p>默认值:32768<br>作用:针对TIME-WAIT,配置其上限。如果降低这个值,可以显著的发现time-wait的数量减少,但系统日志中可能出现如下记录:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">kernel: TCP: time wait bucket table overflow."</span><br></pre></td></tr></table></figure></p><p>对应的,如果升高这个值,可以显著的发现time-wait的数量增加。综合考虑,保持默认值。</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.ipv4.tcp_max_orphans=3276800</span><br></pre></td></tr></table></figure><p>查询命令:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/ipv4/tcp_max_orphans</span><br></pre></td></tr></table></figure><p>默认值:32768<br>作用:orphans的最大值</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.ipv4.tcp_max_syn_backlog=819200</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/ipv4/tcp_max_syn_backlog</span><br></pre></td></tr></table></figure></p><p>默认值:256<br>作用:增大SYN队列的长度(SYN_REVD状态的连接个数),容纳更多连接</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.ipv4.tcp_keepalive_intvl=30</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/ipv4/tcp_keepalive_intvl</span><br></pre></td></tr></table></figure></p><p>默认值:75<br>作用:TCP保活机制,探测失败后,间隔若干秒后重新探测</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.ipv4.tcp_keepalive_probes=3</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/ipv4/tcp_keepalive_probes</span><br></pre></td></tr></table></figure></p><p>默认值:9<br>作用:TCP保活机制,探测失败后,最多尝试探测几次</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.ipv4.tcp_keepalive_time=1200</span><br></pre></td></tr></table></figure><p>查询命令:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/ipv4/tcp_keepalive_time</span><br></pre></td></tr></table></figure><p>默认值:7200<br>作用:TCP保活机制,间隔多久发送1次keepalive探测包</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.netfilter.nf_conntrack_tcp_timeout_established=600</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established</span><br></pre></td></tr></table></figure></p><p>默认值:432000<br>作用:设置 conntrack tcp 状态的超时时间,如果系统出现下述异常时要考虑调整:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ping: sendmsg: Operation not permitted.</span><br><span class="line">kernel: nf_conntrack: table full, dropping packet.</span><br></pre></td></tr></table></figure><p>参考<a href="http://www.linuxidc.com/Linux/2012-11/75151.htm" target="_blank" rel="noopener"><strong>这里</strong></a>还有<a href="http://blog.csdn.net/dog250/article/details/9318843" target="_blank" rel="noopener"><strong>这里</strong></a></p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.netfilter.nf_conntrack_max=655350</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/netfilter/nf_conntrack_max</span><br></pre></td></tr></table></figure></p><p>默认值:262144<br>作用:设置 conntrack 的上限,如果系统出现下述异常时要考虑调整:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ping: sendmsg: Operation not permitted.</span><br><span class="line">kernel: nf_conntrack: table full, dropping packet.</span><br></pre></td></tr></table></figure><p>参考<a href="https://blog.yorkgu.me/2012/02/09/kernel-nf_conntrack-table-full-dropping-packet/" target="_blank" rel="noopener"><strong>这里</strong></a>还有<a href="http://www.cnblogs.com/mydomain/archive/2013/05/19/3087153.html" target="_blank" rel="noopener"><strong>这里</strong></a></p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.core.netdev_max_backlog=500000</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/core/netdev_max_backlog</span><br></pre></td></tr></table></figure></p><p>默认值:1000<br>作用:网卡设备队列长度(请求个数)</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.core.somaxconn=65536</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/core/somaxconn</span><br></pre></td></tr></table></figure></p><p>默认值:128<br>作用:已经成功建立连接等待被应用程序接受(accept调用)的(ESTABLISHED)队列长度(等待被接受的连接个数)</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.core.rmem_default=8388608</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/core/rmem_default</span><br></pre></td></tr></table></figure></p><p>默认值:212992<br>作用:默认的TCP数据接收窗口大小字节数</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.core.wmem_default=8388608</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/core/wmem_default</span><br></pre></td></tr></table></figure></p><p>默认值:212992<br>作用:默认的TCP数据发送窗口大小字节数</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.core.wmem_max=16777216</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/core/wmem_max</span><br></pre></td></tr></table></figure></p><p>默认值:212992<br>作用:最大的TCP数据发送窗口大小字节数</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.core.rmem_max=16777216</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/core/rmem_max</span><br></pre></td></tr></table></figure></p><p>默认值:212992<br>作用:最大的TCP数据接收窗口大小字节数</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.ipv4.tcp_mem=94500000 915000000 927000000</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/ipv4/tcp_mem</span><br></pre></td></tr></table></figure></p><p>默认值:93381 124511 186762<br>作用:内存使用的下限 警戒值 上限</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.ipv4.tcp_rmem=4096 87380 16777216</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/ipv4/tcp_rmem</span><br></pre></td></tr></table></figure></p><p>默认值:4096 87380 6291456<br>作用:socket接收缓冲区内存使用的下限 警戒值 上限</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.ipv4.tcp_wmem=4096 16384 16777216</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/ipv4/tcp_wmem</span><br></pre></td></tr></table></figure></p><p>默认值:4096 16384 4194304<br>作用:socket发送缓冲区内存使用的下限 警戒值 上限</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.ipv4.tcp_thin_dupack=1</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/ipv4/tcp_thin_dupack # Ubuntu系统中未找到此变量</span><br></pre></td></tr></table></figure></p><p>默认值:0<br>作用:收到dupACK时要去检查tcp stream是不是thin(less than 4 packets in flight)</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.ipv4.tcp_thin_linear_timeouts=1</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/ipv4/tcp_thin_linear_timeouts</span><br></pre></td></tr></table></figure></p><p>默认值:0<br>作用:重传超时后要去检查tcp stream是不是thin(less than 4 packets in flight)</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">net.unix.max_dgram_qlen=30000</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/net/unix/max_dgram_qlen</span><br></pre></td></tr></table></figure></p><p>默认值:512<br>作用:UDP队列里数据报的最大个数</p><hr><p>针对lvs,关闭网卡LRO/GRO功能。现在大多数网卡都具有LRO/GRO功能,即网卡收包时将同一流的小包合并成大包 (tcpdump抓包可以看到>MTU 1500bytes的数据包) 交给内核协议栈。LVS内核模块在处理 >MTU 的数据包时,会将其丢弃。因此,如果我们用LVS来传输大文件,很容易出现丢包,传输速度慢。</p><p>解决方法,关闭LRO/GRO功能,命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">ethtool -k eth0 #查看LRO/GRO当前是否打开</span><br><span class="line">ethtool -K eth0 lro off #关闭GRO</span><br><span class="line">ethtool -K eth0 gro off #关闭GRO</span><br></pre></td></tr></table></figure></p><hr><h2 id="Kernel"><a href="#Kernel" class="headerlink" title="Kernel"></a>Kernel</h2><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">kernel.randomize_va_space=1</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/kernel/randomize_va_space</span><br></pre></td></tr></table></figure></p><p>默认值:2<br>作用:内核的随机地址保护模式</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">kernel.panic=1</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/kernel/panic</span><br></pre></td></tr></table></figure></p><p>默认值:0<br>作用:内核panic时,1秒后自动重启</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">kernel.core_pattern=core_%e</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/kernel/core_pattern</span><br></pre></td></tr></table></figure></p><p>默认值:|/usr/share/apport/apport %p %s %c %d %P<br>作用:程序生成core时的文件名格式</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">kernel.sysrq=0</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/kernel/sysrq</span><br></pre></td></tr></table></figure></p><p>默认值:176</p><p>作用:是否启用sysrq功能</p><hr><h2 id="VM"><a href="#VM" class="headerlink" title="VM"></a>VM</h2><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vm.min_free_kbytes=901120</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/vm/min_free_kbytes</span><br></pre></td></tr></table></figure></p><p>默认值:67584<br>作用:保留内存的最低值</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vm.panic_on_oom=1</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/vm/panic_on_oom</span><br></pre></td></tr></table></figure></p><p>默认值:0<br>作用:发生oom时,自动转换为panic</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vm.min_free_kbytes=1048576</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/vm/min_free_kbytes</span><br></pre></td></tr></table></figure></p><p>默认值:67584<br>作用:保留最低可用内存</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vm.swappiness=20</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/vm/swappiness</span><br></pre></td></tr></table></figure></p><p>默认值:60<br>作用:数值(0-100)越高,越可能发生swap交换</p><hr><h2 id="File-System"><a href="#File-System" class="headerlink" title="File System"></a>File System</h2><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">fs.inotify.max_user_watches=8192000</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/fs/inotify/max_user_watches</span><br></pre></td></tr></table></figure></p><p>默认值:524288<br>作用:inotify的watch数量</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">fs.aio-max-nr=1048576</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/fs/aio-max-nr</span><br></pre></td></tr></table></figure></p><p>默认值:65536<br>作用:aio最大值</p><hr><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">fs.file-max=1048575</span><br></pre></td></tr></table></figure><p>查询命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cat /proc/sys/fs/file-max</span><br></pre></td></tr></table></figure></p><p>默认值:804894<br>作用:文件描述符的最大值</p><hr><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><hr><p>[1]<a href="http://yangrong.blog.51cto.com/6945369/1567427" target="_blank" rel="noopener">Linux内核高性能优化【生产环境实例】</a><br>[2]<a href="http://yangrong.blog.51cto.com/6945369/1321594" target="_blank" rel="noopener">linux内核参数解释说明</a><br>[3]<a href="http://www.cnblogs.com/lulu/p/4149312.html" target="_blank" rel="noopener">tcp_tw_reuse、tcp_tw_recycle 使用场景及注意事项</a><br>[4]<a href="http://blog.51cto.com/nosmoking/1684114" target="_blank" rel="noopener">linux内核调优参数对比和解释</a></p>]]></content>
<categories>
<category> Operating System </category>
</categories>
<tags>
<tag> Linux </tag>
<tag> Kernel </tag>
<tag> Tuning </tag>
</tags>
</entry>
<entry>
<title>网络测量之NetSight(NSDI-2014)</title>
<link href="/2018/05/18/Network/Measurement/related-work-netsight/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><p><a href="https://www.usenix.org/conference/nsdi14" target="_blank" rel="noopener"><strong>NSDI 2014</strong></a>年中,由斯坦福大学”SDN之父”,<strong>Nick Mckeown</strong>教授带领的实验室发表了题为《I Know What Yout Packet Did Last Hop: Using Packet Histories to Troubleshoot Networks》的论文。基于本文设计的<strong>NetSight</strong>系统,运行在端系统之上,并利用已有的<strong>OpenFlow</strong>交换机的功能特性,实现了<strong>Packet-Level</strong>的网络数据监测。本文尝试概述了该系统的核心思想,作为网络测量领域相关工作的一次备忘。<br><a id="more"></a></p><h2 id="Paper"><a href="#Paper" class="headerlink" title="Paper"></a>Paper</h2><hr><p>Proposed in <a href="https://www.usenix.org/conference/nsdi14" target="_blank" rel="noopener">NSDI.2014</a><br>-<a href="https://www.usenix.org/system/files/conference/nsdi14/nsdi14-paper-handigol.pdf" target="_blank" rel="noopener"><strong>《I Know What Yout Packet Did Last Hop: Using Packet Histories to Troubleshoot Networks》</strong></a></p><h2 id="Idea"><a href="#Idea" class="headerlink" title="Idea"></a>Idea</h2><hr><p>在本文提出的<strong>NetSight</strong>系统中,在网络中的每一个数据包流经的每一跳都会为该数据包产生一个称作<strong>Postcard</strong>的日志条目(包含Switch ID、输出端口号以及转发状态的版本号),并发送至服务器集群,在集群内部实现负载均衡(压缩->洗牌->解压),对于某个数据包,其<strong>Postcard</strong>最终会被发送至同一个服务器进行处理,在该服务器处,关于此数据包的全部<strong>Postcard</strong>会被根据网络拓扑信息组装成为<strong>Packet History</strong>并作持久化存储,它将可以回答所有关于此数据包在网络中的经历的问题。网络用户基于<strong>NetSight</strong>提供的API开发用于调试的各类应用程序,将其所关注的网络事件或行为通过文中提出的<strong>Packet History Filter(PHF)</strong>进行表达,并在已有的<strong>Packet History</strong>处执行查询匹配(分为实时的(Live)和历史的(Historical)),以获知网络事件是否发生。</p><h2 id="Terminology"><a href="#Terminology" class="headerlink" title="Terminology"></a>Terminology</h2><hr><p>① <strong>Packet History</strong> 定义如下:</p><blockquote><p>The route a packet takes through a network plus the switch state and header mofification it encounters at each hop.</p></blockquote><p>即<strong>数据包历史</strong>,包含该数据包流经网络采用的路由(路径),和它在经过每一跳(交换机)时,交换机的状态以及在本跳受到的首部修改情况。<br><strong>Packet History</strong>反映了该<strong>Packet</strong>在网络中的“种种遭遇”,它提供给我们如下信息:</p><ul><li><strong>What</strong> the packet looked like as it entered the network (Headers)</li><li><strong>Where</strong> the packet was forwarded (Switches + Ports)</li><li><strong>How</strong> it was changed (Header Modification)</li><li><strong>Why</strong> is was forwarded that way (Matched Flow/Actions + Flow Table)</li></ul><p>② <strong>Packet History Filter(PHF)</strong>:一种类似正则表达式的语言,可十分方便简洁地为所关注的数据包历史指定需匹配的 <strong>数据包路径</strong>、<strong>交换机状态</strong>和<strong>数据包首部字段</strong>。</p><p>③ <strong>Postcard</strong>:每个数据包在流经每个交换机时,都会生成一个称作 <strong>Postcard</strong> 的事件记录,它是包括数据包首部、交换机ID、输出端口号以及交换机转发状态的版本号等信息的一个拷贝。最终,同一个数据包流经各个交换机时产生的<strong>Postcard</strong>都在同一台处理服务器处,根据事先知道的网络拓扑信息,重新组装成为一个关于此数据包的<strong>Packet History</strong>,用于匹配用户查询。</p><p>④ <strong>Postcard Filter(PF)</strong>:<strong>PHF</strong>语言中的原子元素(最小元素),用于在某一跳处匹配一个数据包。<strong>PF</strong>实际上是以下字段过滤器的联合:数据包首部、交换机ID(数据路径ID,dpid)、输入端口号(inport)、输出端口号(outport)和该数据包“遭遇”的交换机状态(用一个版本号表示,version)。<strong>PF</strong>可写作如下形式:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">--bpf [not] <BPF> --dpid [not] <switch ID> --inport [not] <input port> -- outport [not] <output port> --version [not] <version></span><br></pre></td></tr></table></figure><p><strong>BPF</strong>是Berkeley Packet Filter表达式。<strong>[not]</strong>是可选项,表示当前字段不可取后面的值。一个<strong>PF</strong>至少应该包含上述字段的中的一个。</p><p>例如:现要匹配一个数据包,它的源IP为A,中途经过交换机S,其中输入端口不能是P。对应PF可写作如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">--bpf "ip src A" --dpid S --inport not P</span><br></pre></td></tr></table></figure><h2 id="Challenge"><a href="#Challenge" class="headerlink" title="Challenge"></a>Challenge</h2><hr><ol><li><strong>Path Visibility</strong>,即<strong>路径可见性</strong>,我们必须以某种方式观察到并记录每个数据包采用的路径</li><li><strong>State Visibility</strong>,即<strong>状态可见性</strong>,我们必须重新构建每个数据包在每一跳观察到的准确的交换机状态</li><li><strong>Modification Visibility</strong>,即<strong>(首部)修改可见性</strong>,我们必须知道数据包在何处以及如何被修改</li><li><strong>Collection Scale</strong>,即<strong>信息收集可扩展性</strong>,上述功能都必须在最大网络速率环境中正常运行</li><li><strong>Storage Scale</strong>,即<strong>信息存储可扩展性</strong>,在某些时候,提供<strong>Packet History</strong>查询功能需要存储几乎所有信息</li><li><strong>Processing Scale</strong>,即<strong>信息处理可扩展性</strong>,查询处理必须与信息收集与存储的速度相匹配</li></ol><h2 id="Application"><a href="#Application" class="headerlink" title="Application"></a>Application</h2><hr><h3 id="ndb-Interactive-Network-Debugger"><a href="#ndb-Interactive-Network-Debugger" class="headerlink" title="ndb: Interactive Network Debugger"></a>ndb: Interactive Network Debugger</h3><hr><p><strong>ndb</strong>是一种交互式的网络调试工具,允许网络应用开发者基于异常网络事件设置相应的<strong>PHF</strong>,一旦事件发生,返回得到的<strong>Packet History</strong>将会包含一系列的造成该异常事件的交换机转发事件,从而帮助诊断如下常见问题:</p><ol><li>Reachability Error(可达性错误)</li><li>Race Condition(竞争条件)</li><li>Incorrect Packet Modification(不正确的数据包修改)</li></ol><h3 id="netwatch-Live-Invariant-Monitor"><a href="#netwatch-Live-Invariant-Monitor" class="headerlink" title="netwatch: Live Invariant Monitor"></a>netwatch: Live Invariant Monitor</h3><hr><p><strong>netwatch</strong>允许运维人员以不变体(不变式,invariant)的形式指明其所关注的网络行为,并在某数据包违反该不变式时触发警告。<br><strong>netwatch</strong>本质上是一个不变式的库,包含了各种用<strong>PHF</strong>写就的不变式,用于匹配违反不变式的数据包。一旦对应的<strong>PHF</strong>被推送至<strong>NetSight</strong>系统中并被下发配置完成后,回调(callback)就会将违反此不变式行为的<strong>Packet History</strong>返回。<br><strong>netwatch</strong>现能支持以下网络不变式:</p><ol><li>Isolation(通信区域隔离)</li><li>Loop Freedom(无环路径)</li><li>Waypoint Routing(路径点路由)</li><li>Max Path Length(最大路径长度)</li></ol><h3 id="netshark-Network-wide-Path-Aware-Packet-Logger"><a href="#netshark-Network-wide-Path-Aware-Packet-Logger" class="headerlink" title="netshark: Network-wide Path-Aware Packet Logger"></a>netshark: Network-wide Path-Aware Packet Logger</h3><hr><p><strong>netshark</strong>允许用户对整个<strong>Packet History</strong>设置过滤器,包括它们在每一跳的路径和首部字段值。<br><strong>netshark</strong>接受一个来自用户定义的<strong>PHF</strong>,返回匹配该查询的<strong>Packet History</strong>,它还包括一个<strong>wireshark</strong>组件用于解析收集到的结果。用户能够观察到某一跳处的数据包属性(包头字段值、交换机ID、输入/输出端口号和其所匹配的流表的版本号),以及<strong>Packet History</strong>的属性(所属路径、所属路径长度等)。</p><h3 id="nprof-Hierachial-Network-Profiler"><a href="#nprof-Hierachial-Network-Profiler" class="headerlink" title="nprof: Hierachial Network Profiler"></a>nprof: Hierachial Network Profiler</h3><hr><p><strong>nprof</strong>帮助用户分析任意链路集合,以理解造成当前链路利用率情况的流量特征和路由决定。<br><strong>nprof</strong>会将收集到的<strong>Packet History</strong>与拓扑信息组合起来已提供一个生动的分层视图,表明是哪些交换机在发送流量给当前链路,以及流量大小。</p><h2 id="Achitecture"><a href="#Achitecture" class="headerlink" title="Achitecture"></a>Achitecture</h2><hr><p><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/Measurement/related-work-netsight/architecture.png" alt="image"></p><p><strong>NetSight</strong>使用一个<strong>Central Coordinator</strong>来管理多个<strong>Worker(NetSight Server)</strong>。<strong>NetSight Application(应用程序)</strong>将基于<strong>PHF</strong>的钩子(Trigger)和查询发送至<strong>Coordinator</strong>,后者则将匹配得到的<strong>Packet History</strong>返回给前者。</p><p><strong>Coordinator</strong>设置从<strong>Switch</strong>到<strong>NetSight Server</strong>的<strong>Postcard</strong>传输和从网络控制平面到<strong>Coordinator</strong>的<strong>State Change Record(状态变化记录)</strong>传输。</p><p>最后,<strong>Coordinator</strong>会对其所管理的<strong>Worker</strong>们执行周期性的存活检测、查询与钩子的广播,并在组装<strong>Packet History</strong>时与之交换拓扑信息。</p><h2 id="Work-Flow"><a href="#Work-Flow" class="headerlink" title="Work Flow"></a>Work Flow</h2><hr><p><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/Measurement/related-work-netsight/workflow.png" alt="image"></p><h3 id="Postcard-Generation"><a href="#Postcard-Generation" class="headerlink" title="Postcard Generation"></a>Postcard Generation</h3><hr><blockquote><p>Goal: Record all information relevant to a forwarding event and send for analysis.</p></blockquote><p>当一个数据包经过一个交换机时,交换机通过复制(<strong>Duplicate</strong>)该包生成一个<strong>Postcard</strong>,随后将其裁剪(<strong>Truncate</strong>)至最小包大小(<strong>Minimun Packet Size</strong>),使用相关状态对其进行标记(<strong>Marking</strong>),并将其转发(<strong>Forwarding</strong>)至<strong>NetSight Servers</strong>进行分析。</p><h3 id="Postcard-Collection"><a href="#Postcard-Collection" class="headerlink" title="Postcard Collection"></a>Postcard Collection</h3><hr><blockquote><p>Goal: To send all postcards for a packet to one server, so that its packet history can be assembled.</p></blockquote><p>为了重建某一个数据包的<strong>History</strong>,<strong>NetSight</strong>需要在单个<strong>Server</strong>中收集到与该数据包相关的所有<strong>Postcard</strong>(一组<strong>Postcard</strong>)。而为了扩展集群的处理能力,<strong>NetSight</strong>需要确保这些<strong>Postcard</strong>组在集群服务器之间实现负载均衡,通过对数据包的<strong>Flow ID</strong>,即五元组(5-tuple:Src/Dest IP, Src/Dest Port, Protocol)进行哈希操作来确保局部性(同一个流的数据包的<strong>Postcard</strong>会被最终发往同一个服务器进行处理)。</p><p>为实现负载均衡而执行的<strong>洗牌(Shuffle)</strong>过程,是基于时间划分的,每个时间段称作一个<strong>Round</strong>,只有在每一个<strong>Round</strong>末尾,服务器会将在这个<strong>Round</strong>时间段内收集到的<strong>Postcard</strong>发送至其通过<strong>Shuffle</strong>过程确定的最终目的地服务器,在那里等待被装配。</p><p>而在<strong>Shuffle</strong>之前,利用相同流(<strong>Within a flow</strong>)和相邻流(<strong>Between flow</strong>)之间的数据包在头部字段的冗余,还可实现<strong>Postcard</strong>的压缩处理,进一步减小<strong>Shuffling</strong>过程中的带宽开销。</p><p>从上图中可以看到,最先接收<strong>Postcard</strong>并对其做压缩和转发的服务器,和最终接收<strong>Postcard</strong>并做装配(以得到同一个数据包的<strong>History</strong>)和存储的服务器,其实是同一个集群,这相当于让<strong>Postcards</strong>分别以原型和被压缩后的形式在网络中被转发了两次,那既然你前面说了要基于流的五元组使得同一个流的数据包的<strong>Postcards</strong>会被发送到同一个服务器作存储,那我岂不是一开始就让交换机根据五元组的哈希值来对<strong>Postcard</strong>进行转发,这不是也可以保证流数据的局部性吗?为什么还要那么复杂地再加一个<strong>Shuffle</strong>的过程,直接发到目的服务器处理和存储不就好了吗?</p><p>这个问题在论文后面的内容才有解释:数据包的五元组是可能被交换机修改的,比如经过NAT交换机时(交换机和路由器其实在网络差不多等价了,不用太在意二层还是三层,默认都有三层转发的功能),数据包的源IP就会被修改,如果仅仅根据五元组哈希值进行转发,经过NAT交换机之前的(包括正在NAT交换机中的)数据包的五元组就是原始五元组,会被转发到某台服务器 $S_1$ 上,但是经过NAT交换机之后的数据包的五元组被修改了,又会被转发到另一台服务器 $S_2$ 上,即会使得同一个数据包在不同跳生成的<strong>Postcard</strong>被转发到不同服务器上去处理和存储,这就违背了流数据的局部性原则,不利于利用冗余进行压缩以减小存储开销的目的。</p><p>为方便之后的装配和存储过程,对<strong>Postcard</strong>的转发有如下两个局部性要求:</p><ol><li>同一个数据包的全部<strong>Postcards</strong>应该被转发至同一个服务器,便于将它们组装成该数据包的<strong>History</strong>,同时不同<strong>Postcard</strong>之间仅交换机元数据不同,存在大量数据冗余(如首部),也便于后期压缩以减少存储开销</li><li>同一个流的所有数据包的全部<strong>Postcards</strong>应该被转发至同一个服务器,不同数据包的<strong>Postcard</strong>也存在数据冗余(如五元组),便于后期压缩以减少存储开销</li></ol><p>基于这个问题,<strong>NetSight</strong>给出的解法就是:增加一个额外的<strong>Shuffle</strong>过程,也就是在交换机生成<strong>Postcard</strong>直到<strong>Postcard</strong>最终被发往目的服务器进行处理和转发的整个过程中,包含两个基于哈希的转发过程。具体来说是这样:针对NAT可能修改五元组这种流标识的问题,利用<strong>IP ID</strong>、<strong>Offset</strong>和<strong>TCP Sequence Number</strong>构成一个组合字段,称为<strong>Packet ID</strong>,这些头部字段显然不会被转发过程中任何一跳修改,NAT也不会修改,所以为<strong>Immutable Headers</strong>,那么第一个转发过程,即交换机发送<strong>Postcard</strong>到服务器时,就基于<strong>Packet ID</strong>的哈希值转发,显然这能够保证同一个数据包的全部<strong>Postcards</strong>会被发送到同一个服务器,这很好。但是,同一个流中的不同数据包的<strong>Packet ID</strong>显然不同,因此还是会被转发到不同服务器,于是加入第二个转发过程,即<strong>Shuffle</strong>过程,服务器现在接收到了某个数据包的全部<strong>Postcards</strong>,将它们关联起来用一个数据结构存储(类似链表),并根据拓扑信息将其排序,而第一跳<strong>Postcard</strong>的五元组,由于它一定未被NAT修改,还是原始的五元组,那么就根据第一跳<strong>Postcard</strong>的五元组的哈希值进行二次转发,是将这个<strong>Postcard</strong>对应数据包的全部<strong>Postcards</strong>都转发到该哈希值对应的服务器处,这样就能够保证同一个流的所有数据包的全部<strong>Postcards</strong>最终被转发到同一个服务器。</p><h3 id="History-Assembly"><a href="#History-Assembly" class="headerlink" title="History Assembly"></a>History Assembly</h3><hr><blockquote><p>Goal: To assemble packet histories from out-of-order postcards.</p></blockquote><p>在某台<strong>NetSight</strong>服务器上,由于从交换机发来数据不同的传播与排队时延(Propagation and Queuing Delay),同一个数据包的<strong>Postcards</strong>很有可能是乱序到达的,在装配成<strong>History</strong>之前,<strong>NetSight</strong>会利用拓扑信息而非时间戳来将属于各个数据包的<strong>Postcards</strong>排好序。</p><p>一旦某<strong>NetSight</strong>服务器从所有其它服务器上收到了某个数据包在全部<strong>Round</strong>时段的<strong>Postcards</strong>,它将会对这些<strong>Postcards</strong>执行解压缩,并将其合并至一个称作<strong>Path Table</strong>的数据结构中,该结构帮助将同一个数据包的全部<strong>Postcards</strong>组成成为一个组(<strong>Group</strong>)。为了标识某个数据包的全部<strong>Postcards</strong>,<strong>NetSight</strong>使用数据包中从不会被更改的头部字段,如<strong>IP ID</strong>、<strong>Fragment Offset</strong>和<strong>TCP Sequence Number</strong>等,组合成为一个新字段,叫作<strong>Packet ID</strong>,它可用来唯一标识某个流中的一个数据包。<strong>Path Table</strong>简单地采用<strong>Packet ID</strong>作为索引(<strong>Key</strong>),而对应的值(<strong>Value</strong>)则为此数据包对应的全部<strong>Postcard</strong>组成的列表。</p><p><strong>NetSight</strong>服务器随后会提取这些已组织好的<strong>Postcard</strong>组,一次提取一组,即一个数据包的全部<strong>Postcard</strong>信息,将其组装成为该数据包的<strong>Packet History</strong>。对于每一组,<strong>NetSight</strong>使用<strong>Switch ID</strong>和<strong>Output Port</strong>,以及实际的拓扑信息,对其执行拓扑排序。而最终排序好的<strong>Postcard</strong>列表即该数据包的<strong>Packet History</strong>。</p><h3 id="Filter-Trigger"><a href="#Filter-Trigger" class="headerlink" title="Filter Trigger"></a>Filter Trigger</h3><hr><blockquote><p>Goal: To immediately notify applications of fresh packet histories matching a pre-installed PHF.</p></blockquote><p>一旦某个数据包的<strong>Packet History</strong>被组装完成,<strong>NetSight</strong>会将其与所有由如<strong>netwatch</strong>等应用程序预先安装下发的(活跃的、实时的)<strong>Live PHF</strong>进行匹配,并在成功匹配某项后立即触发向对应应用程序的通知。</p><h3 id="History-Achival"><a href="#History-Achival" class="headerlink" title="History Achival"></a>History Achival</h3><hr><blockquote><p>Goal: To efficiently store the full set of the packet histories.</p></blockquote><p>在每一个<strong>Round</strong>中生成的<strong>Packet History</strong>的数据流将被写入到一个文件。在写入持久化存储的过程中,<strong>NetSight</strong>会采用与之前提到的一样的压缩算法,都是利用了同一个数据包的<strong>Postcard</strong>之间的与同个流中的数据包之间的头部信息冗余,来实现压缩以减小存储开销。</p><h3 id="History-Queries"><a href="#History-Queries" class="headerlink" title="History Queries"></a>History Queries</h3><hr><blockquote><p>Goal: To enable applications to issure PHF queries against archived packet histories.</p></blockquote><p>当一个应用程序下发对于某一具体时间范围内的历史性的<strong>Historical PHF</strong>查询需求时,该查询会在所有<strong>NetSight</strong>服务器上并行执行。在这里,由于压缩帮助提高了有效磁盘吞吐率,从而也缩短了查询完成时间。</p><h2 id="Implementation"><a href="#Implementation" class="headerlink" title="Implementation"></a>Implementation</h2><hr><p><strong>NetSight</strong>实现分为两个过程:</p><ol><li>插入到<strong>OpenFlow Controller</strong>与<strong>Switches</strong>之前的<strong>Control Channel Proxy(控制通道代理)</strong>,用于记录配置更改,使用Python实现。</li><li><strong>NetSight Server</strong>,用于执行全部的<strong>Postcard</strong>与<strong>History</strong>相关操作,使用C++实现。</li></ol><h3 id="Flow-Table-State-Recorder"><a href="#Flow-Table-State-Recorder" class="headerlink" title="Flow Table State Recorder"></a>Flow Table State Recorder</h3><hr><p>在SDN环境下,任何网络状态的更改都是由控制器参与和协调完成的,这就提供了一个监控与截获交换机配置更改的绝佳地点。<strong>NetSight</strong>在控制器与<strong>OpenFlow</strong>交换机的控制路径上,实现了一个透明代理,称作<strong>Flow Table State Recorder</strong>,每当控制器向交换机们下发新的流表项,<strong>Recorder</strong>都会截获此信息并存储到数据库中。</p><p>具体来说,<strong>Recorder</strong>工作流程如下:对于每条由控制器发送给交换机的<strong>OpenFlow</strong>规则,即<strong>OpenFlow</strong>流表项,<strong>Recorder</strong>都会将其截获并存储,同时在该规则对应的<strong>Action</strong>操作中追加一个新<strong>Action</strong>,令交换机除了对其所接收到匹配该规则的数据包进行正常转发之外,还为其生成一个<strong>Postcard</strong>并发送至<strong>NetSight</strong>服务器。这些追加的<strong>Action</strong>实际就是指示交换机创建一个匹配本条目的数据包的一份拷贝,并用当前交换机ID、输出端口号以及所匹配的流表项的版本号对其进行标记。其中,流表项的版本号就是一个简单的计数器,随着每条流修改信息的到来而递增。这些标记会覆盖原数据包的目的MAC地址字段,因此此数据包,实际就是<strong>Postcard</strong>无需像原数据包一样转发,只需发送到指定<strong>NetSight</strong>服务器处即可,因此目的MAC地址实际就没有作用了。一旦该<strong>Postcard</strong>被生成,随后会基于一个单独的VLAN字段值转发至对应的<strong>NetSight</strong>服务器。<strong>Postcard</strong>可采用两种网络转发模式,通过一个单独的网络处理即<strong>Out-of-band(带外)</strong>传输,和在普通网络中传输即<strong>In-band</strong>传输。当采用后者时,交换机会通过一个特殊的VLAN字段标记识别出<strong>Postcard</strong>,从而避免为<strong>Postcard</strong>再生成<strong>Postcard</strong>,造成不必要的带宽开销。</p><h3 id="NetSight-Server"><a href="#NetSight-Server" class="headerlink" title="NetSight Server"></a>NetSight Server</h3><hr><p>作为实际的<strong>End-host(端系统)</strong>测量解决方案,本文提出的<strong>NetSight</strong>系统并没有涉及对交换机的修改,转而充分利用了<strong>OpenFlow</strong>交换机提供的已有特性,相比<strong>Flow Table State Recorder</strong>仅仅追加规则使得交换机能够生成<strong>per-packet</strong>的<strong>Postcard</strong>,整个<strong>NetSight Server</strong>集群才是整个<strong>NetSight</strong>系统的核心,毕竟是它完成了全部<strong>Postcard</strong>的收集、组装、存储和查询的任务。</p>]]></content>
<categories>
<category> Network </category>
</categories>
<tags>
<tag> Measurement </tag>
<tag> End-Host </tag>
<tag> Packet-Level </tag>
<tag> NSDI </tag>
</tags>
</entry>
<entry>
<title>素数筛选算法</title>
<link href="/2018/05/12/Algorithm/Algorithm/primer/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><p>最近学习了一种<strong>筛素数</strong>的方法,能够以时间复杂度<strong>O(n)</strong>,即线性时间完成。一开始不能理解其中的一句话,搜索了很久,大部分结果都是一群人在网上卖萌。好好思索了一番,按照自己的思路终于理解了。本文的内容绝不卖萌,但也难称严谨,仅以备忘,欢迎斧正。<br><a id="more"></a></p><h2 id="暴力法"><a href="#暴力法" class="headerlink" title="暴力法"></a>暴力法</h2><hr><p>没接触这种方法之前,如果面试官让我筛一下素数,即给定上限 $n$,找出从 $1$ 到 $n$ 之间所有的素数/质数)<br>我大概率会说:(作谦虚状)好的,我尽力试一试。<br>其实心里暗喜:嗯,很轻松嘛,然后不假思索写下…</p><p>就像这样:<br><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">PrintPrimer</span><span class="params">(<span class="keyword">int</span> n)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> i = <span class="number">2</span>; i <= n; i++)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> j = <span class="number">2</span>; j < i; j++)</span><br><span class="line"> <span class="keyword">if</span>(i % j == <span class="number">0</span>)</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">if</span>(j == i)</span><br><span class="line"> <span class="built_in">cout</span> << i << <span class="built_in">endl</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>正准备提交时,突然听到对面一声叹息…不经意望去,对方面露鄙夷,心觉不妙…<br>再看看自己刚写的代码,我的天!遍历???还可以更low一点吗…估计此时面试官和我都想问同一个问题:你到底有没有学过算法?</p><p>于是两秒钟的自我检讨之后,赶紧改了上面代码的几个判断条件,成了这样:<br><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">PrintPrimer</span><span class="params">(<span class="keyword">int</span> n)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> i = <span class="number">3</span>; i <= n; i += <span class="number">2</span>)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> j = <span class="number">3</span>; j <= <span class="built_in">sqrt</span>(i); j += <span class="number">2</span>)</span><br><span class="line"> <span class="keyword">if</span>(i % j == <span class="number">0</span>)</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">if</span>(j * j > i)</span><br><span class="line"> <span class="built_in">cout</span> << i << <span class="built_in">endl</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>嗯…至少不用遍历,对每个数只用检查到其平方根,另外还可以要判断的数从3开始每次加2可以跳过全部偶数,因为偶数肯定不是素数啦,运算次数是降低了不少,可复杂度不还是 $O(n^2)$ 吗?</p><p>不对…对面那家伙脸色不太好,好像更加不耐烦了…怎么办,不慌不慌…</p><h2 id="筛法"><a href="#筛法" class="headerlink" title="筛法"></a>筛法</h2><hr><p>于是,我再度埋下头,看起来像是在认真思考,其实只是不敢直视对方…</p><p>哎,慢着!灵机一闪,思绪回到了大二算法课上,老师讲过一种叫做<strong>“筛法”</strong>的东东,不过好像记不太清了,我再想想…</p><p>半分钟后…</p><p>回来了,我感到它们全都回来了!</p><p>拍拍脑袋后奋笔疾书,<strong>筛法</strong>跃然纸上:<br><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">PrintPrimer</span><span class="params">(<span class="keyword">int</span> n)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">bool</span> is_primer[n]; <span class="comment">// 标志位数组,记录下标对应数字是否被筛除掉</span></span><br><span class="line"> <span class="built_in">memset</span>(is_primer, <span class="literal">true</span>, n);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> i = <span class="number">2</span>; i <= n; i++)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">if</span>(is_primer[i])</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> j = <span class="number">2</span> * i; j <= n; j += i)</span><br><span class="line"> is_primer[j] = <span class="literal">false</span>; <span class="comment">// 访问到一个素数时,就将其倍数都标记为非素数 </span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> i = <span class="number">2</span>; i <= n; i++)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">if</span>(is_primer[i])</span><br><span class="line"> <span class="built_in">cout</span> << i << <span class="built_in">endl</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>这会儿人自信多了,压箱底的老本被翻了出来,总不能有差了,直勾勾地望向面试官,只见他面色稍宽,眉宇间仍透露着几分不满,说道:我看你换了几种算法了,前面的就不说了,给你一个大数据的场景,比如1~1000000的范围,输出其中的素数,你这种筛法的时间性能还能看嘛?</p><p>嗯…毫不留情,莫非还有更优的算法?</p><p>“您容我再想想哈~”,陪着笑脸说完,双手抱头痛苦思考状/(ㄒoㄒ)/~~ 我的神呐…还有啥,还能怎么筛?</p><p>(以下纯属脑洞)<br>闭上眼睛思考的间隙,我去到未来,也就是现在啦,学会了这种线性筛素数的方法。</p><p>╭(╯^╰)╮哼!等我回来,甩你一脸,叫你不耐烦!没(T)错(T)!说的就是你!</p><h2 id="线性筛法"><a href="#线性筛法" class="headerlink" title="线性筛法"></a>线性筛法</h2><hr><p>贫了半天,不废话了,直接上代码,据说是某搞OI的大神写出来的,来源已无从考证:<br><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">PrintPrimer</span><span class="params">(<span class="keyword">int</span> n)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">bool</span> check[n]; <span class="comment">// 标志位数组,判断与下标对应的数字是否为素数</span></span><br><span class="line"> <span class="keyword">int</span> prime[n]; <span class="comment">// 存储素数</span></span><br><span class="line"> <span class="built_in">memset</span>(check, <span class="literal">true</span>, n);</span><br><span class="line"> <span class="built_in">memset</span>(prime, <span class="number">0</span>, n);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">int</span> pos = <span class="number">0</span>; <span class="comment">// prime数组当前位置下标</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> i = <span class="number">2</span>; i <= n; i++)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">if</span>(check[i]) <span class="comment">// i是素数</span></span><br><span class="line"> prime[pos++] = i;</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> j = <span class="number">0</span>; j < pos && i * prime[j] <= n; j++)</span><br><span class="line"> {</span><br><span class="line"> check[i * prime[j]] = <span class="literal">false</span>; <span class="comment">// 筛掉,i * prime[j]不是素数</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span>(i % prime[j] == <span class="number">0</span>)</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> i = <span class="number">0</span>; i < pos; i++)</span><br><span class="line"> <span class="built_in">cout</span> << prime[i] << <span class="built_in">endl</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>以上算法其实有个名字,即<strong>欧拉筛法</strong>,专门用于筛选素数,思想也不复杂:当一个数为素数的时候,它的倍数肯定不是素数。所以可以从2开始通过乘积筛掉所有的合数,将所有合数标记,<strong>保证不被重复筛除</strong>,时间复杂度为 $O(n)$,由于它复杂度是线性的,所以特别适合于大数据量的场景。</p><p>咋一看,算法阐述起来和普通的筛法并无二致,实际上,两者最重要的区别就在于:</p><blockquote><p><strong>有无重复筛除</strong>?</p></blockquote><p>为什么有这个问题呢?我们不妨回顾一下:</p><p>在普通筛法中,假设当前访问到一个素数2,那么接下来就会将指定范围内的2的倍数全部标记为非素数,比如 $6=2\times3$,即在当前访问到的素数为2时,6会被2筛除。当2的倍数被筛除完毕,应该访问下一个素数3,而 $6=3\times2$,即6也会被3筛除,这就造成了重复筛除,使得普通筛法的时间复杂度无法达到线性。</p><p>那么,欧拉筛法是如何做到不重复的筛除呢?一句话概括就是:</p><blockquote><p><strong>每个数都只按不超过其最小质因数的质数来筛除其倍数</strong></p></blockquote><p>比如2,其最小质因数为2,不超过2的质数只有2一个,因此,遍历到2时就只会筛除 $2\times2=4$,而不会筛除6,10,14等更大的2的质数倍的数。<br>再比如5,其最小质因数为5,不超过5的质数有2,3和5,因此,遍历到5时就只会筛除 $5\times2=10$,$5\times3=15,$5\times5$,而不去筛除35,55,65等更大的5的质数倍的数。</p><p>到这里我们理解了思想,到底要如何实现呢?再回头看看本节开篇的那段代码:</p><p>用最笨的方法来看,我们手写出算法的执行过程,试图从中找到规律:</p><hr><blockquote><p>当 $i=2$ 时,$prime[0]=2,pos=1$,此时进入内层 $for$ 循环:<br>$j=0$ 时,会筛除掉 $i \times prime[j]=2\times2=4$,接下来判断 $i \% prime[j]=2 \% 2=0$,故跳出内层循环,从而本轮外循环也结束。</p></blockquote><hr><blockquote><p>当 $i=3$ 时,$prime[1]=3,pos=2$,此时进入内层 $for$ 循环:<br>$j=0$ 时,会筛除掉 $i \times prime[j]=3\times2=6$,接下来判断 $i \% prime[j]=3 \% 2 \neq 0$,继续内层循环。<br>$j=1$ 时,会晒出掉 $i \times prime[j]=3\times3=9$,接下来判断 $i \% prime[j]=3 \% 3=0$,故跳出内层循环,从而本轮外循环也结束。</p></blockquote><hr><blockquote><p>当 $i=4$ 时,已经被2筛除,非素数,此时直接进入内层 $for$ 循环:<br>$j=0$ 时,会筛除掉 $i \times prime[j]=4\times2=8$,接下来判断 $i \% prime[j]=4 \% 2=0$,故跳出内层循环,从而本轮外循环也结束。</p></blockquote><hr><blockquote><p>当 $i=5$ 时,$prime[2]=5,pos=3$,此时进入内层 $for$ 循环:<br>$j=0$ 时,会筛除掉 $i \times prime[j]=5\times2=10$,接下来判断 $i \% prime[j]=5 \% 2 \neq 0$,继续内层循环。<br>$j=1$ 时,会筛除掉 $i \times prime[j]=5\times3=15$,接下来判断 $i \% prime[j]=5 \% 3 \neq 0$,继续内层循环。<br>$j=2$ 时,会筛除掉 $i \times prime[j]=5\times5=25$,接下来判断 $i \% prime[j]=5 \% 5=0$,故跳出内层循环,从而本轮外循环也结束。</p></blockquote><hr><p>从以上执行过程,不难发现:</p><blockquote><p>当 $i$ 为素数时,会首先将自己添加到素数存储数组中 $prime$ 中,然后进入内层 $for$ 循环中筛除其倍数,直至 $i \% prime[j]==0$,而 $i$ 是素数,仅有一个质因数,即其本身,也就是说当前遍历到的数为 $i$ 时,会筛除 $i$ 与全部不超过其最小质因数($i$ 本身)的素数之积;</p><p>当 $i$ 为非素数时,已经被前面的素数筛除掉,即不能将自己添加到素数存储数组 $prime$ 中,因此直接进入内层 $for$ 循环中筛选其倍数,直至 $i \% prime[j]==0$,而 $i$ 是非素数,可能有多个质因数,而要满足该跳出循环的条件,$prime[j]$ 就是 $i$ 的最小质因数,从而会在内层循环中筛除 $i$ 与全部不超过其最小质因数($prime[j]_{min}$)的素数之积。</p></blockquote><p>整合两种情况,得出以下结论:</p><blockquote><p><strong>每次遍历到一个数 $i$,无论素数与否,都会筛除数 $i$ 与其全部不超过其最小质因数的素数之积</strong></p></blockquote><p>还是不够直观是吧,那再看下面这张表:</p><table><thead><tr><th style="text-align:center">$i$</th><th style="text-align:center">$prime[0] \times i$</th><th style="text-align:center">$prime[1] \times i$</th><th style="text-align:center">$prime[2] \times i$</th><th style="text-align:center">$prime[3] \times i$</th><th style="text-align:center">$prime[4] \times i$</th></tr></thead><tbody><tr><td style="text-align:center">2</td><td style="text-align:center">$2 \times 2$</td><td style="text-align:center"></td><td style="text-align:center"></td><td style="text-align:center"></td><td style="text-align:center"></td></tr><tr><td style="text-align:center">3</td><td style="text-align:center">$3 \times 2$</td><td style="text-align:center">$3 \times 3$</td><td style="text-align:center"></td><td style="text-align:center"></td><td style="text-align:center"></td></tr><tr><td style="text-align:center">4</td><td style="text-align:center">$4 \times 2$</td><td style="text-align:center"></td><td style="text-align:center"></td><td style="text-align:center"></td><td style="text-align:center"></td></tr><tr><td style="text-align:center">5</td><td style="text-align:center">$5 \times 2$</td><td style="text-align:center">$5 \times 3$</td><td style="text-align:center">$5 \times 5$</td><td style="text-align:center"></td><td style="text-align:center"></td></tr><tr><td style="text-align:center">6</td><td style="text-align:center">$6 \times 2$</td><td style="text-align:center"></td><td style="text-align:center"></td><td style="text-align:center"></td><td style="text-align:center"></td></tr><tr><td style="text-align:center">7</td><td style="text-align:center">$7 \times 2$</td><td style="text-align:center">$7 \times 3$</td><td style="text-align:center">$7 \times 5$</td><td style="text-align:center">$7 \times 7$</td><td style="text-align:center"></td></tr><tr><td style="text-align:center">8</td><td style="text-align:center">$8 \times 2$</td><td style="text-align:center"></td><td style="text-align:center"></td><td style="text-align:center"></td><td style="text-align:center"></td></tr><tr><td style="text-align:center">9</td><td style="text-align:center">$9 \times 2$</td><td style="text-align:center">$9 \times 3$</td><td style="text-align:center"></td><td style="text-align:center"></td><td style="text-align:center"></td></tr><tr><td style="text-align:center">10</td><td style="text-align:center">$10 \times 2$</td><td style="text-align:center"></td><td style="text-align:center"></td><td style="text-align:center"></td><td style="text-align:center"></td></tr><tr><td style="text-align:center">11</td><td style="text-align:center">$11 \times 2$</td><td style="text-align:center">$11 \times 3$</td><td style="text-align:center">$11 \times 5$</td><td style="text-align:center">$11 \times 7$</td><td style="text-align:center">$11 \times 11$</td></tr></tbody></table><p>第一列即筛除掉全部以2为最小质因数的数,第二列筛除掉全部以3为最小质因数的数…依次类推,可以把所有的合数都筛掉。</p><p>因为是按照最小素因子筛选,所以可以保证每个数都只会被筛一遍。</p><p>上面是我的通俗理解,下面援引自<a href="https://oi.abcdabcd987.com/sieve-prime-in-linear-time/" target="_blank" rel="noopener"><strong>此篇</strong></a>,感觉分析得更为严谨,也放在这里供大家参考:</p><p>这段代码最难理解的是这句:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (i % prime[j] == <span class="number">0</span>) <span class="keyword">break</span>;</span><br></pre></td></tr></table></figure><blockquote><p>要理解这句话,(顺便不严谨地)证明这个算法的时间复杂度和正确性,要从以下两个方面:</p><p><strong>每个数至少被访问一次</strong><br>对于质数,一定会在 $i$ 的循环中访问到,并确定为质数。<br>对于合数,因为每一个合数都可以表示成它最小的质因数和另一个数的乘积,而我们枚举了所有的另一个数(也就是 $i$),所以它一定会被它的最小质因数筛掉。</p><p><strong>每个数至多被访问一次</strong><br>对于质数,不可能在 $j$ 的循环中被访问到,因此仅会在 $i$ 的循环中被访问到恰好一次。<br>对于合数,对于 $i = i_1 = p \times a$,因为在 $i_1 \% prime[j_1] == 0$ 时 $break$,所以不可能出现一个数 $x=i_1 \times prime[k]=p \times a \times prime[k] (k > j_1)$ 在 $i = i_1, j = k$ 的时候被筛掉一次,又在 $i = a \times prime[k]$ 的时候被 $p$ 给筛掉的情况。</p><p>综上所述,每个数被访问一次且仅访问一次!因此整个算法的复杂度是 $O(n)$ 的。</p></blockquote><h2 id="面试结果"><a href="#面试结果" class="headerlink" title="面试结果"></a>面试结果</h2><hr><p>hmmmmmmmm…<br>当然,很愉快的,即使是在面试官迟到了1小时的情况下,TT还是很给面子,没让我过,我记住了,哼!<br>不过好事多磨,总有收获还是不错的啦~再接再厉!</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><hr><p>[1]<a href="https://www.jianshu.com/p/f16d318efe9b" target="_blank" rel="noopener">菜鸟学线性筛素数</a><br>[2]<a href="https://www.cnblogs.com/A-S-KirigiriKyoko/articles/6034572.html" target="_blank" rel="noopener">欧拉筛法找素数</a><br>[3]<a href="https://www.jianshu.com/p/afaff2e916b7" target="_blank" rel="noopener">求1000000以内的素数</a><br>[4]<a href="https://oi.abcdabcd987.com/sieve-prime-in-linear-time/" target="_blank" rel="noopener">线性时间内筛素数和欧拉函数</a></p>]]></content>
<categories>
<category> Algorithm </category>
</categories>
<tags>
<tag> Algorithm </tag>
<tag> Primer </tag>
</tags>
</entry>
<entry>
<title>数据结构之栈与队列(优先队列/堆)</title>
<link href="/2018/05/03/Algorithm/DataStructure/stack-queue-priority_queue-heap/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><hr><p><strong>栈</strong>与<strong>队列</strong>是两种重要的特殊线性表,从结构上讲,两者都是线性表,但从操作上讲,两者支持的基本操作却只是线性表操作的子集,是操作受限制的线性表。栈与队列两者最大的区别在于,栈元素后进先出(LIFO,Last In First Out),而队列元素先进先出(FIFO,First In First Out)。此外,针对队列这一特殊数据结构,有时需考虑队列元素的优先级的关系,即根据用户自定义的优先级排序,出队时优先弹出优先级更高(低)的元素,<strong>优先队列</strong>能更好地满足实际问题中的需求,而在优先队列的各种实现中,<strong>堆</strong>是一种最高效的数据结构。本文分别介绍了<strong>顺序栈</strong>、<strong>链式栈</strong>、<strong>链式队列</strong>和<strong>循环队列</strong>以及对应与前两种队列实现的<strong>最大/最小优先级队列</strong>,还有两种堆结构,<strong>最大堆</strong>与<strong>最小堆</strong>的基本结构,并给出了相应的C++类代码实现。<br><a id="more"></a></p><h2 id="栈-Stack"><a href="#栈-Stack" class="headerlink" title="栈(Stack)"></a>栈(Stack)</h2><hr><p>栈是限定只能在表头进行插入(入栈)与删除(出栈)操作的线性表,表头端称为栈顶,表尾端称为栈底。</p><p>设有栈 $S=(a_1, a_2,…, a_n)$ ,则一般称 $a_1$ 为栈底元素,$a_n$为栈顶元素,按 $a_1, a_2,…,a_n$ 的顺序依次进栈,则根据元素入栈的规定,从栈顶到栈底的元素依次为 $a_n,…,a_2, a_1$,出栈时弹出的第一个元素为栈顶元素,即 $a_n$,也就是说栈是按<strong>后进先出</strong>的原则进行。故,栈可称为<strong>后进先出(Last In First Out, LIFO)</strong>的线性表。</p><p>栈有顺序表和链表两种实现方式。</p><h3 id="顺序栈-Sequence-Stack"><a href="#顺序栈-Sequence-Stack" class="headerlink" title="顺序栈(Sequence Stack)"></a>顺序栈(Sequence Stack)</h3><hr><p>在顺序实现中,利用一组地址连续的存储单元即数组依次存放从栈底到栈顶的数据元素,将数据类型为<code>ElemType</code>的数据元素存放在数组中,并用<code>count</code>存储数组中存储的栈的实际元素个数。每次插入新的栈顶元素,如栈未满,则操作成功,<code>count</code>值加一,而当删除栈顶元素时,如栈不空,操作成功,并且<code>count</code>值减一。</p><p>顺序栈的类声明及定义如下:</p><table><thead><tr><th style="text-align:center">Header</th><th style="text-align:center">Implementation</th></tr></thead><tbody><tr><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/SqStack.h" target="_blank" rel="noopener"><strong>SqStack.h</strong></a></td><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/SqStack.cc" target="_blank" rel="noopener"><strong>SqStack.cc</strong></a></td></tr></tbody></table><h3 id="链式栈-Link-Stack"><a href="#链式栈-Link-Stack" class="headerlink" title="链式栈(Link Stack)"></a>链式栈(Link Stack)</h3><hr><p>在程序中同时使用多个栈的情况下,使用链式栈不仅可以提高存储效率,同时还可以达到共享存储空间的目的。链表实现的栈,入栈和出栈都非常简单,一般也都不使用头结点直接实现。</p><p>链式栈的类声明及定义如下:</p><table><thead><tr><th style="text-align:center">Header</th><th style="text-align:center">Implementation</th></tr></thead><tbody><tr><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/LinkStack.h" target="_blank" rel="noopener"><strong>LinkStack.h</strong></a></td><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/LinkStack.cc" target="_blank" rel="noopener"><strong>LinkStack.cc</strong></a></td></tr></tbody></table><h2 id="队列-Queue"><a href="#队列-Queue" class="headerlink" title="队列(Queue)"></a>队列(Queue)</h2><hr><p>队列是一种先进先出(First In First Out,FIFO)的线性表,只允许在一端进行插入(入队)操作,而在另一端进行删除(出队)操作。</p><p>在队列中,允许入队操作的一端称为队尾,允许出队操作的一端称为队头。</p><p>设有队列 $q=(a_1, a_2,…,a_n)$,则 $a_1$ 称为队头元素,$a_n$ 称为队尾元素,队列中元素是按 $a_1, a_2,…,a_n$ 的顺序入队,同时也要求按照相同的顺序出队。</p><p>队列也有两种存储结构:顺序存储结构和链式存储结构。</p><h3 id="链队列-Link-Queue"><a href="#链队列-Link-Queue" class="headerlink" title="链队列(Link Queue)"></a>链队列(Link Queue)</h3><hr><p>用链表表示的队列称为链队列,一个链队列应用两个分别指示队头与队尾的指针,分别称为头指针(<code>front</code>)和尾指针(<code>rear</code>)。其中头指针指向该队列的头结点,即不包含实际数据域的结点,而尾指针指向队列最后一个结点,是包含实际数据域的结点。</p><p>如果要从队列中弹出一个元素,必须从单链表的第一个结点中取出队头元素,并删除此节点,而入队的新元素是存放在队尾处的,也就是单链表的最后一个元素的后面,并且此结点将成为新的队尾。</p><p>链队列适合于数据元素个数变动比较大的情况,一般不存在溢出的问题,如果程序中要使用多个队列,最好使用链队列,这样将不会出现存储分配的问题,也不必进行数据元素的移动。</p><p>链队列的类声明及定义如下:</p><table><thead><tr><th style="text-align:center">Header</th><th style="text-align:center">Implementation</th></tr></thead><tbody><tr><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/LinkQueue.h" target="_blank" rel="noopener"><strong>LinkQueue.h</strong></a></td><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/LinkQueue.cc" target="_blank" rel="noopener"><strong>LinkQueue.cc</strong></a></td></tr></tbody></table><h3 id="循环队列-Circular-Queue"><a href="#循环队列-Circular-Queue" class="headerlink" title="循环队列(Circular Queue)"></a>循环队列(Circular Queue)</h3><hr><p>如果用C++描述队列的顺序存储结构,实际是利用一个一位数组<code>elem</code>作为队列的元素存储结构,并分别设立了两个指针<code>front</code>和<code>rear</code>分别表示队头和队尾,<code>maxSize</code>是队列的最大元素个数。</p><p>但这样容易导致一个问题,试想,假设新创建一个队列,最大空间<code>maxSize</code>为5,那么依次入队5个元素,此时<code>front</code>依然指向队头,即数组第一个位置之前的位置,<code>rear</code>则指向队尾,即数组最后一个位置,然后出队2个元素,即从队头开始弹出2个元素,即删除数组<code>elem</code>前两个元素,此时队头指针<code>front</code>后移至数组第2个位置(有效元素从第3个位置,<code>front</code>永远指向第1个有效元素所在位置之前的一个位置)。若此时想再入队一个新元素,即从队尾插入新元素,但是由于队尾指针<code>rear</code>已经到达数组最后一个位置,不能再后移,即不允许再插入元素,而数组前2个位置由于之前的两次出队操作空出了2个位置,即此时队列的实际可用空间还没有使用完,这种情况下再插入一个新元素产生的溢出称为<strong>“假溢出”</strong>。</p><p>解决<strong>假溢出</strong>的一个较巧妙的方法是将顺序队列从逻辑上看成一个环,成为一个循环队列,循环队列的首尾相接,当队头<code>front</code>和队尾<code>rear</code>进入到<code>maxSize-1</code>时,再进一个位置就自动移动到0,可用取余运算(%)简单地实现:</p><blockquote><p>队头前进1个位置:<code>front = (front + 1) % maxSize</code><br>队尾前进1个位置:<code>rear = (rear + 1) % maxSize</code></p></blockquote><p>但仅仅这样处理带来一个问题,不难发现当队列为满时,有 <code>front == rear</code>,而当队列为空时,依然有 <code>front == rear</code>。<br>可知,仅从<code>front == rear</code>这一条件并不能判断究竟是队空还是队满,那我们也有两种处理方法:</p><ol><li>另设一个标志位区别是队空还是队满</li><li>少用一个元素空间,约定队头在队尾指针的下一个位置时作为队满的标志</li></ol><p>本文采用第2种,即浪费一个元素空间方法,那么在这种情况下:</p><blockquote><p>队空时,有<code>front == rear</code><br>队满时,有<code>(rear - front + maxSize) % maxSize == maxSize - 1</code></p></blockquote><p>而队列中的元素个数即为 <code>(rear - front + maxSize) % maxSize</code>,在牺牲一个位置不存储元素的情况下,若元素个数 <code>== maxSize - 1</code>,当然队列也就是满的。 </p><p>循环队列的类声明及定义如下:</p><table><thead><tr><th style="text-align:center">Header</th><th style="text-align:center">Implementation</th></tr></thead><tbody><tr><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/CircQueue.h" target="_blank" rel="noopener"><strong>CircQueue.h</strong></a></td><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/CircQueue.cc" target="_blank" rel="noopener"><strong>CircQueue.cc</strong></a></td></tr></tbody></table><h2 id="优先级队列-Priority-Queue"><a href="#优先级队列-Priority-Queue" class="headerlink" title="优先级队列(Priority Queue)"></a>优先级队列(Priority Queue)</h2><p>在许多情况下,前面介绍的普通队列是不够用的,先进先出的机制有时需要某些优先规则来完善使其更能适用于实际场景。比如在医院中,病危患者应具有更高的优先级,若还是按先来后到顺序对排队患者依次治疗,显然是不合理的,也就是说,当医生有空时,应立刻从患者中选择病情最危急者优先救治,此处的患者的病情危重程度就决定了其就诊的优先级。</p><p>一般地,优先级高低实际就决定了队列中元素的出队顺序。</p><p>优先队列是一种基于队列并同时考虑了优先级的数据结构,其中元素的固有顺序决定了对基本操作的执行结果,优先队列有两种类型:最小优先队列和最大优先队列。最小优先队列的出队操作<code>OutQueue()</code>将删除最小的数据元素值,最大优先队列的出队操作<code>OutQueue()</code>将删除最大的数据元素值。</p><p>优先队列有多种实现方法,一种比较简单的实现方法是在作入队操作<code>InQueue()</code>时,元素不是排在队列的末尾,而是根据其优先级将其插入到队列中的合适位置,使队列的元素有序,优先队列类可作为队列类的派生类来实现,只需覆盖队列类的入队操作<code>InQueue()</code>即可。</p><p>基于链队列和循环队列,每种队列又可派生出最大、最小两种优先队列,共计四种优先队列的类声明及定义如下:</p><table><thead><tr><th style="text-align:center">Header</th><th style="text-align:center">Implementation</th></tr></thead><tbody><tr><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/MaxPriorityLinkQueue.h" target="_blank" rel="noopener"><strong>MaxPriorityLinkQueue.h</strong></a></td><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/MaxPriorityLinkQueue.cc" target="_blank" rel="noopener"><strong>MaxPriorityLinkQueue.cc</strong></a></td></tr><tr><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/MinPriorityLinkQueue.h" target="_blank" rel="noopener"><strong>MinPriorityLinkQueue.h</strong></a></td><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/MinPriorityLinkQueue.cc" target="_blank" rel="noopener"><strong>MinPriorityLinkQueue.cc</strong></a></td></tr><tr><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/MaxPriorityCircQueue.h" target="_blank" rel="noopener"><strong>MaxPriorityCircQueue.h</strong></a></td><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/MaxPriorityCircQueue.cc" target="_blank" rel="noopener"><strong>MaxPriorityCircQueue.cc</strong></a></td></tr><tr><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/MinPriorityCircQueue.h" target="_blank" rel="noopener"><strong>MinPriorityCircQueue.h</strong></a></td><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/MinPriorityCircQueue.cc" target="_blank" rel="noopener"><strong>MinPriorityCircQueue.cc</strong></a></td></tr></tbody></table><h2 id="堆-Heap"><a href="#堆-Heap" class="headerlink" title="堆(Heap)"></a>堆(Heap)</h2><hr><p>在上节介绍优先队列时提到,构造优先队列的方法是通过简单地在普通队列将新元素入队时,为其按优先级高低(元素值大小)找到合适的位置再插入,而不是直接插入在队尾,这种方式得到的优先队列的元素是严格有序排列的,如最大优先队列中,元素从大到小排列,最大元素即队头元素。这种优先队列的实现,思想简单,但同时开销也比较大,效率不够高,本节会介绍一种叫做堆的数据结构,在优先队列的各种实现中,堆是最高效的一种数据结构。</p><p>不难想象,若数据集合严格有序,将为各种操作带来遍历,如上节的优先队列的实现,但是,有些场景并不要求数据全部有序,或者在操作开始前就完全有序。在许多应用中,通常需要收集一部分数据,从中挑选具有最小或最大关键码(优先级)的记录开始处理。接着,可能会收集更多数据,并处理当前数据集中具有最小或最大关键码的记录。对于此类应用,我们期望的数据结构,应能支持插入操作,并能方便地从中取出具有最小或最大关键码的记录,这样的数据结构其实就是优先队列。也就是说,优先队列仅仅要求能够方便地找到数据中关键码最小或最大,即优先级最低或最高的记录,其实并不要求数据严格排好序,并能保证出队时总能找到关键码最小或最大的记录优先出队,堆正好可以满足这一需求,而堆是局部有序的,并非严格有序。</p><p>假定在各个数据记录或元素中,存在一个能够标识数据记录或元素的数据项,并将依据该数据项对数据进行组织,则可称此数据项为关键码(key)。</p><p>如果有一个关键码的集合,把它的所有元素按<strong>完全二叉树</strong>的顺序存储方式存放在一个一位数组中。如果有:</p><blockquote><p>每个父结点的关键码总比其孩子结点的关键码大<br>于是位于堆顶(即二叉树根结点)的结点的关键码最大,故称该集合为<strong>最大堆</strong></p></blockquote><p>或者:</p><blockquote><p>每个父结点的关键码总比其孩子结点的关键码小<br>于是位于堆顶(即二叉树根结点)的结点的关键码最小,故称该集合为<strong>最小堆</strong></p></blockquote><p>在堆中,所有的结点具有称之为<strong>“堆序列”</strong>的关系,同样,“堆序”分为最小堆序和最大堆序。具有最小堆序的结点之间存在小于或等于关系,具有最大堆序的结点之间存在大于或等于的关系。</p><p>根据<strong>完全二叉树</strong>的性质,由堆存储在下标为0开始计数的数组中,因此,在堆(数组)中给定下标为 $i$的结点时:</p><ol><li>如 $i=0$,则结点 $i$ 为根结点,无父结点,否则结点 $i$ 的父结点为结点 $\lfloor\frac{i-1}{2}\rfloor$</li><li>如 $2i+1>n-1$,则结点 $i$ 无左子女,否则结点 $i$ 的左子女为结点 $2i+1$</li><li>如 $2i+2>n-1$,则结点 $i$ 无右子女,否则结点 $i$ 的右子女为结点 $2i+2$</li></ol><p>另外还会用到的一个性质:</p><ul><li>完全二叉树最后一个非叶结点的下标为 $\lfloor\frac{n-2}{2}\rfloor$</li></ul><p>下节会以<strong>最小堆</strong>为例讲解堆的构造和调整过程。</p><h3 id="最小堆"><a href="#最小堆" class="headerlink" title="最小堆"></a>最小堆</h3><hr><p>父结点的关键码总是小于其孩子结点的完全二叉树称为最小堆。</p><h4 id="堆的构造"><a href="#堆的构造" class="headerlink" title="堆的构造"></a>堆的构造</h4><p>当给出一个记录的关键码集合时,首先把它的记录顺序放在堆的<code>Heap</code>数组中,最初数据的排列显示它不是一个最小堆,因此需要把它调整成为一个堆。</p><p>我们采用从下向上逐步调整形成堆的方法:轮流以完全二叉树结点编号从 $\lfloor\frac{n-2}{2}\rfloor$ ,即完全二叉树最后一个非叶结点开始,一直到编号为0,即根结点,调用下滑调整算法<code>SiftDown()</code>,将以它们为根的子树调整成为最小堆,从局部到整体,将最小堆逐步扩大,直到在根结点处也调整完成后,整个树即被调整成为最小堆。</p><p><code>SiftDown()</code>是一个自上而下的调整算法,其基本思想是:</p><ol><li>对有 $m$ 个记录的集合 $R$,将它置为完全二叉树的顺序存储。首先从结点 $i$ 开始向下调整,即把以它为根结点的子树调整成为最小堆,前提条件是假定它的两棵子树都已成为最小堆(所以一开始要从完全二叉树的第一个非叶结点开始整个调整过程,因此非叶结点的子树即叶子结点,单个结点本身就是最小堆)。</li><li>如果结点 $i$ 的左孩子的关键码小于右孩子的关键码:$R[j].key < R[j+1].key$,其中 $j=2i+1,即j是i的左孩子,j+1是i的右孩子$,则按结点 $i$ 的左分支进行调整,否则沿右分支调整。让 $j$ 指向参加调整的孩子结点,调整的方法是以 $R[i] 与 R[j]$ 进行关键码的比较:若 $R[i].key > R[j].key$,则把关键码小的孩子 $j$ 上浮到 $i$ 的位置。</li><li>然后 $i和j$ 进一步下沉,即令 $i=j, j=2j+1$,继续向下一层进行比较;若 $R[i].key \leq R[j].key$,则不对调,也不再向下一层继续比较,因为此时可判定以 $i$ 为根结点的子树已经是最小堆了,算法终止。</li><li>最后结果是关键码最小的结点上浮到了堆顶,且完全二叉树的每个局部都是一个最小堆,符合最小堆定义,最小堆形成。</li></ol><p>给出<code>SiftDown()</code>代码实现如下:<br><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <<span class="class"><span class="keyword">class</span> <span class="title">ElemType</span>></span></span><br><span class="line"><span class="class"><span class="title">void</span> <span class="title">MinHeap</span><ElemType>:</span>:siftDown(<span class="keyword">int</span> start, <span class="keyword">int</span> m)</span><br><span class="line">{</span><br><span class="line"> <span class="comment">// 从结点start开始到结点m为止,自上向下比较,如果子女的值小于父结点的值</span></span><br><span class="line"> <span class="comment">// 则关键码更小的子女上浮,继续向下层比较,这样将一个集合局部调整为最小堆</span></span><br><span class="line"> <span class="keyword">int</span> i = start, j = <span class="number">2</span> * i + <span class="number">1</span>; <span class="comment">// j是i的左孩子</span></span><br><span class="line"> ElemType temp = heap[i];</span><br><span class="line"> <span class="keyword">while</span> (j <= m)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">if</span> (j < m && heap[j] > heap[j + <span class="number">1</span>]) <span class="comment">// i的右孩子比j(i的左孩子)更小,j指向右孩子(更小的)</span></span><br><span class="line"> j++;</span><br><span class="line"> <span class="keyword">if</span> (temp <= heap[j]) <span class="comment">// i已经比其子女j小了,调整结束</span></span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> {</span><br><span class="line"> heap[i] = heap[j]; <span class="comment">// i比子女j大,则j上浮替代i的位置,调整位置i和j均下降</span></span><br><span class="line"> i = j;</span><br><span class="line"> j = <span class="number">2</span> * j + <span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> heap[i] = temp; <span class="comment">// 放回start结点</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>以上代码中值得一提的一个细节是:关键码比较时用的是比较运算符<strong>“>”</strong>和<strong>“<=”</strong>,这些运算符应该在元素类型<code>ElemType</code>中重载定义。如果堆中元素各不相同的话,其实可以只需”>”便足够,将<code>temp <= heap[j]</code>改为<code>heap[j] > temp</code>即可,此运算符应该在元素类型<code>ElemType</code>中重载定义。从而可以总结如下规律:</p><blockquote><p>在元素互异的理想情况下,实现最小堆要求元素类型重载<strong>“<“</strong>运算符,而实现最大堆则要求元素类型重载<strong>”>“</strong>运算符。</p></blockquote><p>给出堆的构造函数<code>MinHeap()</code>代码实现如下:<br><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <<span class="class"><span class="keyword">class</span> <span class="title">ElemType</span>></span></span><br><span class="line"><span class="class"><span class="title">MinHeap</span><ElemType>:</span>:MinHeap(ElemType arr[], <span class="keyword">int</span> n)</span><br><span class="line">{</span><br><span class="line"> maxHeapSize = (DEFAULT_SIZE < n) ? n : DEFAULT_SIZE;</span><br><span class="line"> heap = <span class="keyword">new</span> ElemType[maxHeapSize];</span><br><span class="line"> <span class="keyword">if</span> (heap == <span class="literal">nullptr</span>)</span><br><span class="line"> {</span><br><span class="line"> <span class="built_in">cerr</span> << <span class="string">"heap create failed!"</span> << <span class="built_in">endl</span>;</span><br><span class="line"> <span class="built_in">exit</span>(<span class="number">1</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> pos = <span class="number">0</span>; pos < n; pos++)</span><br><span class="line"> heap[pos] = arr[pos];</span><br><span class="line"> currentSize = n;</span><br><span class="line"> <span class="keyword">int</span> currentPos = (currentSize - <span class="number">2</span>) / <span class="number">2</span>; <span class="comment">// 找最初调整位置:最后分支结点(最后一个有孩子的结点)</span></span><br><span class="line"> <span class="keyword">while</span> (currentPos >= <span class="number">0</span>) <span class="comment">// 自底向上逐步调整形成堆</span></span><br><span class="line"> {</span><br><span class="line"> siftDown(currentPos, currentSize - <span class="number">1</span>); <span class="comment">// 局部自上向下下滑调整</span></span><br><span class="line"> currentPos--; <span class="comment">// 再向前换一个分支结点</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><h4 id="堆的插入"><a href="#堆的插入" class="headerlink" title="堆的插入"></a>堆的插入</h4><p>最小堆的插入算法则调用了另一种的调整算法<code>SiftUp()</code>,实现自下而上的上滑调整。因为每次新结点总是插在已经建成的最小堆的后面,这是必须遵循与<code>SiftDown()</code>相反的比较路径,从下而上,与父结点的关键码进行比较、对调。</p><p>给出<code>SiftUp()</code>代码实现如下:<br><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <<span class="class"><span class="keyword">class</span> <span class="title">ElemType</span>></span></span><br><span class="line"><span class="class"><span class="title">void</span> <span class="title">MinHeap</span><ElemType>:</span>:siftUp(<span class="keyword">int</span> start)</span><br><span class="line">{</span><br><span class="line"> <span class="comment">// 从结点start开始到结点0为止,自下而上比较,如果子女的值小于父结点的值</span></span><br><span class="line"> <span class="comment">// 则子女上浮,继续向上层比较,这样将一个集合重新调整为最小堆,元素类型ElemType应自行定义"<="运算符</span></span><br><span class="line"> <span class="keyword">int</span> j = start, i = (j - <span class="number">1</span>) / <span class="number">2</span>;</span><br><span class="line"> ElemType temp = heap[j];</span><br><span class="line"> <span class="keyword">while</span> (j > <span class="number">0</span>) <span class="comment">// 沿父结点路径向上直达根,i是j的父结点</span></span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">if</span> (heap[i] <= temp) <span class="comment">// 父结点值小,无需调整</span></span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> {</span><br><span class="line"> heap[j] = heap[i]; <span class="comment">// i比子女j大,则i下滑取代j的位置,调整位置i和j均上滑</span></span><br><span class="line"> j = i;</span><br><span class="line"> i = (i - <span class="number">1</span>) / <span class="number">2</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> heap[j] = temp;</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>同样地,<code>SiftUp()</code>中用到了比较运算符<strong>“<=”</strong>,它应该在元素类型<code>ElemType</code>中重载定义。</p><p>给出<code>Insert()</code>代码实现如下:<br><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <<span class="class"><span class="keyword">class</span> <span class="title">ElemType</span>></span></span><br><span class="line"><span class="class"><span class="title">bool</span> <span class="title">MinHeap</span><ElemType>:</span>:Insert(<span class="keyword">const</span> ElemType &e)</span><br><span class="line">{</span><br><span class="line"> <span class="keyword">if</span> (currentSize == maxHeapSize) <span class="comment">// 堆满</span></span><br><span class="line"> {</span><br><span class="line"> <span class="built_in">cerr</span> << <span class="string">"Heap already full!"</span> << <span class="built_in">endl</span>;</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> heap[currentSize] = e; <span class="comment">// 每次新元素总会插入到堆末尾</span></span><br><span class="line"> siftUp(currentSize); <span class="comment">// 从当前元素即尾元素开始向上调整</span></span><br><span class="line"> currentSize++;</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><h4 id="堆的删除"><a href="#堆的删除" class="headerlink" title="堆的删除"></a>堆的删除</h4><p>通常,从最小堆中删除具有最小关键码记录的操作是将最小堆的堆顶元素,即其对应完全二叉树的顺序表示的第0号元素删去。把这个元素取走后,一般以堆的最后一个结点填补取走的堆顶元素,并将堆的实际元素个数减1。但是用最后一个元素取代堆顶元素将破坏堆,需要调用<code>SiftDown()</code>算法从堆顶向下调整。</p><p>给出<code>RemoveMin()</code>代码实现如下:<br><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">template</span> <<span class="class"><span class="keyword">class</span> <span class="title">ElemType</span>></span></span><br><span class="line"><span class="class"><span class="title">bool</span> <span class="title">MinHeap</span><ElemType>:</span>:RemoveMin(ElemType &e)</span><br><span class="line">{</span><br><span class="line"> <span class="keyword">if</span> (currentSize == <span class="number">0</span>) <span class="comment">// 堆空</span></span><br><span class="line"> {</span><br><span class="line"> <span class="built_in">cerr</span> << <span class="string">"Heap already empty!"</span> << <span class="built_in">endl</span>;</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> e = heap[<span class="number">0</span>];</span><br><span class="line"> heap[<span class="number">0</span>] = heap[currentSize - <span class="number">1</span>]; <span class="comment">// 最后元素填补到根结点</span></span><br><span class="line"> currentSize--;</span><br><span class="line"> siftDown(<span class="number">0</span>, currentSize - <span class="number">1</span>); <span class="comment">// 因为尾元素放到了堆顶,故从堆顶元素开始自上而下调整</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>从完全二叉树的性质可知,$n$ 个结点的完全二叉树的深度为 $k=\lceil\log{(n+1)}\rceil$,应用堆的调整算法<code>SiftDown()</code>时,<code>while</code>循环次数最大为树的深度减1,所以堆的删除算法的时间复杂度为 $O(\log{n})$。而在插入一个新结点时,使用了一个堆的上滑调整算法<code>SiftUp()</code>,其中<code>while</code>循环次数不超过树的深度减1,所以堆的插入算法的时间复杂度也是 $O(\log{n})$。建树操作执行了 $\lfloor\frac{n}{2}\rfloor$ 次<code>SiftDown()</code>算法,其时间复杂度为 $O(n\log{n})$。</p><p>最小堆的类声明及定义如下:</p><table><thead><tr><th style="text-align:center">Header</th><th style="text-align:center">Implementation</th></tr></thead><tbody><tr><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/MinHeap.h" target="_blank" rel="noopener"><strong>MinHeap.h</strong></a></td><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/MinHeap.cc" target="_blank" rel="noopener"><strong>MinHeap.cc</strong></a></td></tr></tbody></table><h3 id="最大堆"><a href="#最大堆" class="headerlink" title="最大堆"></a>最大堆</h3><hr><p>父结点的关键码总是大于其孩子结点的完全二叉树称为最大堆。</p><p>最大堆的类声明及定义如下:</p><table><thead><tr><th style="text-align:center">Header</th><th style="text-align:center">Implementation</th></tr></thead><tbody><tr><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/MaxHeap.h" target="_blank" rel="noopener"><strong>MaxHeap.h</strong></a></td><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/MaxHeap.cc" target="_blank" rel="noopener"><strong>MaxHeap.cc</strong></a></td></tr></tbody></table><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><hr><p>[1]<a href="https://www.amazon.cn/dp/B001TREOXM" target="_blank" rel="noopener">数据结构与算法(C++版) - 唐宁九主编</a><br>[2]<a href="https://www.amazon.cn/dp/B0011F7UHO" target="_blank" rel="noopener">数据结构(用面向对象方法与C++语言描述) - 殷人昆主编</a></p>]]></content>
<categories>
<category> Algorithm </category>
</categories>
<tags>
<tag> Data Structure </tag>
<tag> C++ </tag>
<tag> Stack </tag>
<tag> Queue </tag>
<tag> Priority Queue </tag>
<tag> Heap </tag>
</tags>
</entry>
<entry>
<title>常见算法之二叉树遍历</title>
<link href="/2018/04/23/Algorithm/Algorithm/binary-tree-traversal/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><hr><p>所谓遍历二叉树,就是遵从某种次序,顺着某一条搜索路径访问二叉树中的各个结点,使得每个结点均被访问一次,而且仅被访问一次。本文详细介绍了二叉树的前序(又称先序)、中序和后序遍历的规则及其算法实现。本文全部代码示例可从<a href="/2018/04/23/Algorithm/Algorithm/binary-tree-traversal/BinTreeTraversal.cc" title="此处">此处</a>获得。<br><a id="more"></a></p><h2 id="遍历的定义"><a href="#遍历的定义" class="headerlink" title="遍历的定义"></a>遍历的定义</h2><hr><p>“遍历”,即访问到二叉树中的所有结点,且每个结点仅被访问一次。“访问”的含义可以很广,如:输出结点的信息、修改结点的数据之等,但一般要求这种访问不破坏原来数据之间的逻辑结构。</p><p>本文中”访问“规定为输出当前遍历结点元素值,定义打印函数如下:<br><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <<span class="class"><span class="keyword">class</span> <span class="title">ElemType</span>></span></span><br><span class="line"><span class="class"><span class="title">void</span> <span class="title">Print</span>(<span class="title">ElemType</span> <span class="title">e</span>)</span></span><br><span class="line"><span class="class">{</span></span><br><span class="line"> <span class="built_in">cout</span> << e;</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>实际上,“遍历”是任何数据结构均有的公共操作,二叉树是非线性结构,每个结点最多有两个后继,则存在如何遍历,即按什么样的搜索路径遍历的问题。这样就必须规定遍历的规则,按此规则遍历二叉树,最后得到二叉树中所有结点组成的一个线性序列。</p><h2 id="二叉树的遍历类型"><a href="#二叉树的遍历类型" class="headerlink" title="二叉树的遍历类型"></a>二叉树的遍历类型</h2><hr><p>根据二叉树的结构特征,可以有三类搜索路径:先上而下的按层次遍历、先左(子树)后右(子树)的遍历、先右(子树)后左(子树)的遍历。设访问根结点记作 $D$,遍历根左子树记作 $L$,遍历根的右子树记作 $R$,则可能的遍历次序有:$DLR、LDR、LRD、DRL、RDL、RLD$ 及层次遍历。若规定先左后右,则只剩下4种遍历方式:$DLR、LDR、LRD$ 及层次遍历,根据根结点被遍历的次序,通常称 $DLR、LDR和LRD$ 这3种遍历为前序遍历、中序遍历和后序遍历。</p><p>给出如下二叉树实例:<img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Algorithm/Algorithm/traversal-binary-tree/example-bintree.png" alt="image"></p><p>则其不同方式的遍历序列分别为:</p><ul><li>层次遍历结果序列:ABCDEFG</li><li>前序遍历结果序列:ABDGCEF</li><li>中序遍历结果序列:DGBAECF</li><li>后序遍历结果序列:GDBEFCA</li></ul><p>后文依次介绍了层次遍历以及前序、中序和后序算法实现,其中用到的两个二叉树相关的数据结构 <a href="https://github.com/sundongxu/data-structure/blob/master/BinTree.h" target="_blank" rel="noopener"><strong>BinTree</strong></a> 和 <a href="https://github.com/sundongxu/data-structure/blob/master/BinTreeNode.h" target="_blank" rel="noopener"><strong>BinTreeNode</strong></a> 可参见<a href="dongdongdong.me"><strong>此篇</strong></a>。</p><p>为简化二叉树遍历代码实现,给出以下二叉树的结点结构:<br><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <<span class="class"><span class="keyword">class</span> <span class="title">ElemType</span>></span></span><br><span class="line"><span class="class"><span class="title">struct</span> <span class="title">BinTreeNode</span></span></span><br><span class="line"><span class="class">{</span></span><br><span class="line"> ElemType val; <span class="comment">// 结点关键码</span></span><br><span class="line"> BinTreeNode *leftChild; <span class="comment">// 左孩子结点</span></span><br><span class="line"> BinTreeNode *rightChild; <span class="comment">// 右孩子结点</span></span><br><span class="line"> BinTreeNode(ElemType v) : val(v), leftChild(<span class="literal">nullptr</span>), rightChild(<span class="literal">nullptr</span>) {} <span class="comment">// 构造函数</span></span><br><span class="line">};</span><br></pre></td></tr></table></figure></p><p>在调用本文中实现的遍历算法时,函数指针参数 <code>Visit</code> 即传入 <code>Print</code> 函数名即可。</p><h2 id="层次遍历-Level-Order-Traversal"><a href="#层次遍历-Level-Order-Traversal" class="headerlink" title="层次遍历(Level-Order Traversal)"></a>层次遍历(Level-Order Traversal)</h2><hr><p>层次遍历是先访问层次小的所有结点,即从根结点开始,同一层次从左到右访问,然后再访问下一层次的结点。根据层次遍历的定义,除根结点外,每个结点都处于其双亲结点的下一层次,而指向每个结点的指针都记录在其双亲结点中,因此为了找到各结点,需将已经访问过的结点的孩子结点保存下来。使用一个 <strong>队列</strong> 来存储已访问过的结点的孩子结点。初始将根结点入栈,每次要访问的下一个结点都是队列上取出指向结点的指针,每访问完一个结点后,如果它有左孩子、右孩子结点,则将它的左、右孩子结点入队,如此重复,直到队列为空,则遍历结束。</p><p>下面给出二叉树<strong>层次遍历</strong>具体实现:<br><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <<span class="class"><span class="keyword">class</span> <span class="title">ElemType</span>></span></span><br><span class="line"><span class="class"><span class="title">void</span> <span class="title">LevelOrderTraverse</span>(<span class="title">BinTreeNode</span><ElemType> *<span class="title">root</span>, <span class="title">void</span> (*<span class="title">Visit</span>)(<span class="title">ElemType</span> &))</span></span><br><span class="line"><span class="class">{</span></span><br><span class="line"> <span class="comment">// 操作结果:层次遍历二叉树</span></span><br><span class="line"> <span class="keyword">if</span> (root == <span class="literal">nullptr</span>) <span class="comment">// 二叉树为空,结束算法</span></span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> <span class="built_in">queue</span><BinTreeNode<ElemType> *> q; <span class="comment">// 辅助队列</span></span><br><span class="line"> BinTreeNode<ElemType> *node = root; <span class="comment">// 从根结点开始进行层次遍历</span></span><br><span class="line"> q.push(node);</span><br><span class="line"> <span class="keyword">while</span> (!q.empty())</span><br><span class="line"> {</span><br><span class="line"> <span class="comment">// 队列非空,说明还有结点未访问</span></span><br><span class="line"> node = q.front();</span><br><span class="line"> q.pop();</span><br><span class="line"> (*Visit)(node->val);</span><br><span class="line"> <span class="keyword">if</span> (node->leftChild != <span class="literal">nullptr</span>) <span class="comment">// 左孩子非空</span></span><br><span class="line"> q.push(node->leftChild); <span class="comment">// 左孩子入队</span></span><br><span class="line"> <span class="keyword">if</span> (node->rightChild != <span class="literal">nullptr</span>) <span class="comment">// 右孩子非空</span></span><br><span class="line"> q.push(node->rightChild); <span class="comment">// 右孩子入队</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><h2 id="前序遍历-Pre-Order-Traversal"><a href="#前序遍历-Pre-Order-Traversal" class="headerlink" title="前序遍历(Pre-Order Traversal)"></a>前序遍历(Pre-Order Traversal)</h2><hr><p>二叉树的前序遍历定义如下:<br>如果二叉树为空,则算法结束。<br>否则:</p><ol><li>访问根结点(D)</li><li>前序遍历左子树(L)</li><li>前序遍历右子树(R)</li></ol><p>前序遍历也称为先序遍历,就是按照“根-左子树-右子树”的次序遍历二叉树。</p><p>前序遍历算法分为递归和非递归实现。</p><h3 id="递归遍历"><a href="#递归遍历" class="headerlink" title="递归遍历"></a>递归遍历</h3><hr><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <<span class="class"><span class="keyword">class</span> <span class="title">ElemType</span>></span></span><br><span class="line"><span class="class"><span class="title">void</span> <span class="title">RecursionPreOrderTraverse</span>(<span class="title">BinTreeNode</span><ElemType> *<span class="title">root</span>, <span class="title">void</span> (*<span class="title">Visit</span>)(<span class="title">ElemType</span> &))</span></span><br><span class="line"><span class="class">{</span></span><br><span class="line"> <span class="comment">// 操作结果:前序遍历以root为根的二叉树(递归)</span></span><br><span class="line"> <span class="keyword">if</span> (root != <span class="literal">nullptr</span>)</span><br><span class="line"> {</span><br><span class="line"> (*Visit)(root->val); <span class="comment">// 访问根结点</span></span><br><span class="line"> PreOrderTraverse(root->leftChild, Visit); <span class="comment">// 递归访问左子树</span></span><br><span class="line"> PreOrderTraverse(root->rightChild, Visit); <span class="comment">// 递归访问右子树</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="非递归遍历"><a href="#非递归遍历" class="headerlink" title="非递归遍历"></a>非递归遍历</h3><hr><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <<span class="class"><span class="keyword">class</span> <span class="title">ElemType</span>></span></span><br><span class="line"><span class="class"><span class="title">void</span> <span class="title">NonRecursionPreOrderTraverse</span>(<span class="title">BinTreeNode</span><ElemType> *<span class="title">root</span>, <span class="title">void</span> (*<span class="title">Visit</span>)(<span class="title">ElemType</span> &))</span></span><br><span class="line"><span class="class">{</span></span><br><span class="line"> <span class="comment">// 操作结果:前序遍历以root为根的二叉树(非递归)</span></span><br><span class="line"> <span class="keyword">if</span> (root == <span class="literal">nullptr</span>) <span class="comment">// 二叉树为空,结束算法</span></span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> <span class="built_in">stack</span><BinTreeNode<ElemType> *> s; <span class="comment">// 辅助栈</span></span><br><span class="line"> BinTreeNode<ElemType> *p; <span class="comment">// 当前遍历结点指针</span></span><br><span class="line"> s.push(root);</span><br><span class="line"> <span class="keyword">while</span> (!s.empty()) <span class="comment">// 栈非空</span></span><br><span class="line"> {</span><br><span class="line"> p = s.top();</span><br><span class="line"> s.pop();</span><br><span class="line"> Visit(p->val); <span class="comment">// 结点出栈即被访问</span></span><br><span class="line"> <span class="keyword">if</span> (p->rightChild != <span class="literal">nullptr</span>)</span><br><span class="line"> s.push(p->rightChild);</span><br><span class="line"> <span class="keyword">if</span> (p->leftChild != <span class="literal">nullptr</span>)</span><br><span class="line"> s.push(p->leftChild);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="中序遍历-In-Order-Traversal"><a href="#中序遍历-In-Order-Traversal" class="headerlink" title="中序遍历(In-Order Traversal)"></a>中序遍历(In-Order Traversal)</h2><hr><p>二叉树的中序遍历定义如下:<br>如果二叉树为空,则算法结束。<br>否则:</p><ol><li>中序遍历左子树(L)</li><li>访问根结点(D)</li><li>中序遍历右子树(R)</li></ol><p>中序遍历就是按照“左子树-根-右子树”的次序遍历二叉树。</p><p>中序遍历算法分为递归和非递归实现。</p><h3 id="递归遍历-1"><a href="#递归遍历-1" class="headerlink" title="递归遍历"></a>递归遍历</h3><hr><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <<span class="class"><span class="keyword">class</span> <span class="title">ElemType</span>></span></span><br><span class="line"><span class="class"><span class="title">void</span> <span class="title">RecursionInOrderTraverse</span>(<span class="title">BinTreeNode</span><ElemType> *<span class="title">root</span>, <span class="title">void</span> (*<span class="title">Visit</span>)(<span class="title">ElemType</span> &))</span></span><br><span class="line"><span class="class">{</span></span><br><span class="line"> <span class="comment">// 操作结果:中序遍历以r为根的二叉树</span></span><br><span class="line"> <span class="keyword">if</span> (root != <span class="literal">nullptr</span>)</span><br><span class="line"> {</span><br><span class="line"> InOrderTraverse(root->leftChild, Visit); <span class="comment">// 递归访问左子树</span></span><br><span class="line"> (*Visit)(root->val); <span class="comment">// 访问根结点</span></span><br><span class="line"> InOrderTraverse(root->rightChild, Visit); <span class="comment">// 递归访问右子树</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="非递归遍历-1"><a href="#非递归遍历-1" class="headerlink" title="非递归遍历"></a>非递归遍历</h3><hr><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <<span class="class"><span class="keyword">class</span> <span class="title">ElemType</span>></span></span><br><span class="line"><span class="class"><span class="title">void</span> <span class="title">NonRecursionInOrderTraverse</span>(<span class="title">BinTreeNode</span><ElemType> *<span class="title">root</span>, <span class="title">void</span> (*<span class="title">Visit</span>)(<span class="title">ElemType</span> &))</span></span><br><span class="line"><span class="class">{</span></span><br><span class="line"> <span class="comment">// 操作结果:中序遍历以root为根的二叉树(非递归)</span></span><br><span class="line"> <span class="keyword">if</span> (root == <span class="literal">nullptr</span>) <span class="comment">// 二叉树为空,结束算法</span></span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> <span class="built_in">stack</span><BinTreeNode<ElemType> *> s; <span class="comment">// 辅助栈</span></span><br><span class="line"> BinTreeNode<ElemType> *p = root; <span class="comment">// 当前遍历结点指针</span></span><br><span class="line"> <span class="keyword">do</span></span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">while</span> (p != <span class="literal">nullptr</span>) <span class="comment">// 遍历指针未到最左下结点,则不空</span></span><br><span class="line"> {</span><br><span class="line"> s.push(p); <span class="comment">// 该子树沿途结点进栈</span></span><br><span class="line"> p = p->leftChild; <span class="comment">// 遍历指针前进到左孩子结点</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!s.empty()) <span class="comment">// 栈不空时退栈</span></span><br><span class="line"> {</span><br><span class="line"> p = s.top();</span><br><span class="line"> s.pop();</span><br><span class="line"> Visit(p->val); <span class="comment">// 结点出栈即被访问</span></span><br><span class="line"> p = p->rightChild; <span class="comment">// 遍历指针前进到右孩子结点</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> } <span class="keyword">while</span> (p != <span class="literal">nullptr</span> || !s.empty()); <span class="comment">// p非空即本轮循环访问的结点还有右孩子 或 栈中还有结点</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="后序遍历-Post-Order-Traversal"><a href="#后序遍历-Post-Order-Traversal" class="headerlink" title="后序遍历(Post-Order Traversal)"></a>后序遍历(Post-Order Traversal)</h2><hr><p>二叉树的前序遍历定义如下:<br>如果二叉树为空,则算法结束。<br>否则:</p><ol><li>后序遍历左子树(L)</li><li>后序遍历右子树(R)</li><li>访问根结点(D)</li></ol><p>后序遍历就是按照“左子树-右子树-根”的次序遍历二叉树。</p><p>后序遍历算法分为递归和非递归实现。</p><h3 id="递归遍历-2"><a href="#递归遍历-2" class="headerlink" title="递归遍历"></a>递归遍历</h3><hr><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> <<span class="class"><span class="keyword">class</span> <span class="title">ElemType</span>></span></span><br><span class="line"><span class="class"><span class="title">void</span> <span class="title">RecursionPostOrderTraverse</span>(<span class="title">BinTreeNode</span><ElemType> *<span class="title">root</span>, <span class="title">void</span> (*<span class="title">Visit</span>)(<span class="title">ElemType</span> &))</span></span><br><span class="line"><span class="class">{</span></span><br><span class="line"> <span class="comment">// 操作结果:后序遍历以r为根的二叉树</span></span><br><span class="line"> <span class="keyword">if</span> (root != <span class="literal">nullptr</span>)</span><br><span class="line"> {</span><br><span class="line"> PostOrderTraverse(root->leftChild, Visit); <span class="comment">// 递归访问左子树</span></span><br><span class="line"> PostOrderTraverse(root->rightChild, Visit); <span class="comment">// 递归访问右子树</span></span><br><span class="line"> (*Visit)(root->val); <span class="comment">// 访问根结点</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="非递归遍历-2"><a href="#非递归遍历-2" class="headerlink" title="非递归遍历"></a>非递归遍历</h3><hr><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">enum</span> Tag</span><br><span class="line">{</span><br><span class="line"> L,</span><br><span class="line"> R</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> <<span class="class"><span class="keyword">class</span> <span class="title">ElemType</span>></span></span><br><span class="line"><span class="class"><span class="title">struct</span> <span class="title">StackNode</span></span></span><br><span class="line"><span class="class">{</span></span><br><span class="line"> <span class="comment">// 在后序遍历非递归实现所用栈结点类定义</span></span><br><span class="line"> BinTreeNode<ElemType> *ptr; <span class="comment">// 指向树结点的指针</span></span><br><span class="line"> Tag tag; <span class="comment">// 该结点的退栈标记</span></span><br><span class="line"> StackNode(BinTreeNode<ElemType> *N = <span class="literal">nullptr</span>) : ptr(N), tag(L) {} <span class="comment">// 构造函数</span></span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> <<span class="class"><span class="keyword">class</span> <span class="title">ElemType</span>></span></span><br><span class="line"><span class="class"><span class="title">void</span> <span class="title">NonRecursionPostOrderTraverse</span>(<span class="title">BinTreeNode</span><ElemType> *<span class="title">root</span>, <span class="title">void</span> (*<span class="title">Visit</span>)(<span class="title">ElemType</span> &))</span></span><br><span class="line"><span class="class">{</span></span><br><span class="line"> <span class="comment">// 操作结果:后序遍历以root为根的二叉树(非递归)</span></span><br><span class="line"> <span class="keyword">if</span> (root == <span class="literal">nullptr</span>) <span class="comment">// 二叉树为空,结束算法</span></span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> <span class="built_in">stack</span><StackNode<ElemType>> s; <span class="comment">// 辅助栈</span></span><br><span class="line"> StackNode<ElemType> w;</span><br><span class="line"> BinTreeNode<ElemType> *p = root; <span class="comment">// 当前遍历结点指针</span></span><br><span class="line"> <span class="keyword">do</span></span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">while</span> (p != <span class="literal">nullptr</span>)</span><br><span class="line"> {</span><br><span class="line"> w.ptr = p;</span><br><span class="line"> w.tag = L;</span><br><span class="line"> s.push(w);</span><br><span class="line"> p = p->leftChild;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">bool</span> shouldContinue = <span class="literal">true</span>;</span><br><span class="line"> <span class="keyword">while</span> (shouldContinue && !s.empty())</span><br><span class="line"> {</span><br><span class="line"> w = s.top();</span><br><span class="line"> s.pop();</span><br><span class="line"> p = w.ptr;</span><br><span class="line"> <span class="keyword">switch</span> (w.tag)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">case</span> L:</span><br><span class="line"> w.tag = R;</span><br><span class="line"> s.push(w);</span><br><span class="line"> shouldContinue = <span class="literal">false</span>;</span><br><span class="line"> p = p->rightChild;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> R:</span><br><span class="line"> Visit(p->val);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">while</span> (!s.empty());</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><hr><p>[1]<a href="https://www.amazon.cn/dp/B0011F7UHO" target="_blank" rel="noopener">数据结构(用面向对象方法与C++语言描述) - 殷人昆主编</a></p>]]></content>
<categories>
<category> Algorithm </category>
</categories>
<tags>
<tag> Algorithm </tag>
<tag> Binary Tree </tag>
<tag> Traversal </tag>
<tag> Pre-Order </tag>
<tag> In-Order </tag>
<tag> Post-Order </tag>
<tag> Recursion </tag>
<tag> NonRecursion </tag>
</tags>
</entry>
<entry>
<title>常见算法之排序</title>
<link href="/2018/04/23/Algorithm/Algorithm/sort/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><p>各类排序算法,不仅是算法基本功,也是面试中永恒的考题,关于每种算法<strong>思想</strong>、<strong>实现</strong>(递归与非递归)以及<strong>时空复杂度分析</strong>是必须牢牢把握的送分题。本文先将排序算法按不同标准进行分类,随后依次详细介绍了直接插入、希尔、冒泡、快速、简单选择、堆、归并、基数与外部排序等经典排序算法,并在文末给出了各种排序方法的性能比较作为总结。本文全部代码实例可从<a href="/2018/04/23/Algorithm/Algorithm/sort/Sort.cc" title="此处">此处</a>获得。<br><a id="more"></a></p><p>排序(Sorting),就是将数据元素(或记录)的任意序列,重新排序成按关键字有序的序列,下文介绍的排序算法实现均针对于整型数组,且最终结果序列按元素<strong>从小到大(即升序)</strong>排列。</p><h2 id="排序分类"><a href="#排序分类" class="headerlink" title="排序分类"></a>排序分类</h2><p>按照排序过程中所涉及的存储器,可将排序分为如下两类:</p><ul><li><p><strong>内部</strong>排序:待排序的数据元素全部存入计算机内存中,在排序过程中无需访问外存(如磁盘)</p></li><li><p><strong>外部</strong>排序:待排序的数据元素不能全部装入内存(如数据量过大),在排序过程中需要不断访问外存</p></li></ul><p>本文仅介绍内部排序的几种经典算法,外部排序则暂不考虑。</p><p>对于内部排序,按排序过程中所依据的思想,又可分为如下四类:</p><ol><li><strong>插入</strong>排序</li><li><strong>交换</strong>排序</li><li><strong>选择</strong>排序</li><li><strong>归并</strong>排序</li></ol><p>按内排序过程中所需工作量(时间复杂度),还可分为如下三类:</p><ol><li>简单排序方法,对应时间复杂度为<strong>$O(n^2)$</strong></li><li>先进排序方法,对应时间复杂度为<strong>$O(n\log{n})$</strong></li><li>基数排序方法,对应时间复杂度为<strong>$O(dn)$</strong></li></ol><p>而根据序列中原本有序的元素的相对位置关系是否在排序前后发生改变,又可分为如下两类:</p><ul><li><p><strong>稳定</strong>排序:在原序列中有两个元素 $a=b$,且 $a$ 位于 $b$ 之前,若在排序后的新序列中 $a$ 仍然位于 $b$ 之前,则称该排序算法是稳定的。稳定的排序算法有:直接插入排序、</p></li><li><p><strong>不稳定</strong>排序:在原序列中有两个元素$a=b$,且 $a$ 位于 $b$ 之前,若在排序后的新序列中 $a$ 不一定位于 $b$ 之前,则称该排序算法是不稳定的。不稳定的排序算法有:</p></li></ul><h2 id="性能评估"><a href="#性能评估" class="headerlink" title="性能评估"></a>性能评估</h2><p>排序算法的执行时间是衡量算法好坏的最重要参数。排序的时间开销可用算法执行中的<strong>数据比较次数</strong>和<strong>数据移动次数</strong>来衡量。一般排序算法运行时间代价的估算都按平均情况(Average Case)进行估算,对于那些受元素初始排列和元素个数影响较大的,则还需要按最好情况(Best Case)和最差情况(Worst Case)。</p><p>本文介绍的排序算法中,简单排序算法,如直接插入排序、冒泡排序和选择排序的(平均)时间开销(复杂度)均为 $O(n^2)$。而更为高效的排序方法,如快速排序、归并排序和堆排序算法,(平均)时间开销(复杂度)均为 $O(n\log{n})$,在本文中,若无特殊说明,则$\log$均表示以2为底的对数。</p><h2 id="插入排序-Insertion-Sort"><a href="#插入排序-Insertion-Sort" class="headerlink" title="插入排序(Insertion Sort)"></a>插入排序(Insertion Sort)</h2><hr><h3 id="直接插入排序-Straight-Insertion-Sort"><a href="#直接插入排序-Straight-Insertion-Sort" class="headerlink" title="直接插入排序(Straight Insertion Sort)"></a>直接插入排序(Straight Insertion Sort)</h3><hr><h4 id="基本思想"><a href="#基本思想" class="headerlink" title="基本思想"></a>基本思想</h4><p>初始化结果序列里面只有第一个数据元素,将第一个数据元素看成一个有序子序列,再以此从第二个数据元素起逐个与结果序列这个有序序列比较,通过比较为当前元素找到合适的位置进行插入,直到插入最后一个数据元素后,整个结果序列数组即有序。</p><p>一般地,在第 $i$ 步上,将 $elem[i]$ 插入到由 $elem[0]$ ~ $elem[i-1]$构成的有序子序列中。</p><h4 id="代码实现"><a href="#代码实现" class="headerlink" title="代码实现"></a>代码实现</h4><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">StraightInsertSort</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="keyword">int</span> n)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">int</span> i = <span class="number">1</span>; i < n; i++)</span><br><span class="line"> {</span><br><span class="line"> <span class="comment">// 进行第i次插入,待插入元素为elem[i]</span></span><br><span class="line"> <span class="comment">// 前面0~i-1都已有序</span></span><br><span class="line"> <span class="keyword">int</span> e = elem[i]; <span class="comment">// 暂存待插入元素</span></span><br><span class="line"> <span class="keyword">int</span> j; </span><br><span class="line"> <span class="keyword">for</span>(j = i - <span class="number">1</span>; j >= <span class="number">0</span> && e < elem[j]; j--)</span><br><span class="line"> {</span><br><span class="line"> <span class="comment">// 将比elem[i]大的都后移一位</span></span><br><span class="line"> elem[j+<span class="number">1</span>] = elem[j]; </span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 结束上述循环即找到令elem[i] >= elem[j]的位置j</span></span><br><span class="line"> <span class="comment">// 此时elem[i]应插入在elem[j]之后一个位置</span></span><br><span class="line"> elem[j+<span class="number">1</span>] = e;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="复杂度分析"><a href="#复杂度分析" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><p>插入排序算法由嵌套的两个 $for$ 循环组成,外层循环执行 $n-1$ 次,内层循环比较复杂,循环次数依赖于第 $i$ 个元素前面的元素值比 $elem[i]$ 大的元素个数。</p><p><strong>最坏情况下</strong>,每个元素都必须移动到数组的最前面,如果原数组是逆序(即降序)的话就会出现此种情况,此时第一趟循环1次,第二趟循环2次,以此类推,第 $i$ 趟循环 $i$次,第 $n-1$ 趟循环 $n-1$ 次,故总比较次数$=1+2+3+…+(n-1)=\frac{n(n-1)}{2}$,即时间复杂度为 $O(n^2)$。</p><p><strong>最好情况下</strong>,即原数组已经递增有序,这是每个内层 $for$ 循环刚进入即退出,只比较一次,而不移动元素,总的比较次数就是外循环次数 $n-1$ 次,可知此时时间复杂度为 $O(n)$。</p><p>另外,如待排序数组元素的顺序是随机的,也就是排序的元素可能出现的各种排序的概率是相同的,可以证明直接插入排序在此<strong>平均情况下</strong>,时间复杂度为 $O(n^2)$。</p><p>从以上讨论可知,直接插入排序的运行时间与待排序元素的初始排列顺序密切相关。</p><p><strong>直接插入排序是一种稳定的排序算法。</strong></p><h3 id="折半插入排序-Binary-Insertion-Sort"><a href="#折半插入排序-Binary-Insertion-Sort" class="headerlink" title="折半插入排序(Binary Insertion Sort)"></a>折半插入排序(Binary Insertion Sort)</h3><hr><h4 id="基本思想-1"><a href="#基本思想-1" class="headerlink" title="基本思想"></a>基本思想</h4><p>初始化结果序列里面只有第一个数据元素,将第一个数据元素看成一个有序子序列,每次插入一个元素时使用折半搜索法为其找到合适的插入位置,直到插入最后一个数据元素后,整个结果序列数组即有序。</p><h4 id="代码实现-1"><a href="#代码实现-1" class="headerlink" title="代码实现"></a>代码实现</h4><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">BinaryInsertSort</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="keyword">int</span> n)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">1</span>; i < n; i++)</span><br><span class="line"> {</span><br><span class="line"> <span class="comment">// 进行第i次插入,待插入元素为elem[i]</span></span><br><span class="line"> <span class="comment">// 前面0~i-1都已有序</span></span><br><span class="line"> <span class="keyword">int</span> e = elem[i]; <span class="comment">// 暂存待插入元素</span></span><br><span class="line"> <span class="keyword">int</span> low = <span class="number">0</span>, high = i - <span class="number">1</span>; <span class="comment">// 范围端点</span></span><br><span class="line"> <span class="keyword">while</span> (low <= high)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">int</span> mid = (low + high) / <span class="number">2</span>;</span><br><span class="line"> <span class="keyword">if</span> (e > elem[mid])</span><br><span class="line"> low = mid + <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">else</span> <span class="comment">// e <= elem[mid]</span></span><br><span class="line"> high = mid - <span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 结束上述循环即找到elem[i]应插入的位置为low</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> j = i - <span class="number">1</span>; j >= low; j--)</span><br><span class="line"> elem[j + <span class="number">1</span>] = elem[j];</span><br><span class="line"></span><br><span class="line"> elem[low] = e;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="复杂度分析-1"><a href="#复杂度分析-1" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><p>折半搜索比顺序搜索快,所以折半插入排序就平均性能来说肯定比直接插入排序要好。它所需要的元素比较次数与待排序元素序列的初始排列无关,而仅仅依赖于元素个数。在插入第 $i$ 个元素时,需要经过 $\lfloor\log{i}\rfloor+1$次元素比较,才能确定它应插入的位置。</p><p>当 $n$ 较大时,总元素的比较次数比直接插入排序的最差情况要好得多,但比其最好情况要差。所以,在元素初始排列已经有序或接近有序时,直接插入排序比折半插入排序所需执行的元素比较次数要少。而两者的元素移动次数相同,依赖于元素的初始排列。</p><p>折半插入排序改变了插入排序算法的比较次数,未改变其待排序数据移动次数。最好情况下,插入的位置,刚好是二分位置,时间复杂度为 $O(n\log{n})$。最坏情况下,时间复杂度为 $O(n^2)$。平均时间复杂度为 $O(n^2)$。</p><p><strong>折半插入排序是一个稳定的排序方法。</strong></p><h3 id="希尔排序-Shell-Sort"><a href="#希尔排序-Shell-Sort" class="headerlink" title="希尔排序(Shell Sort)"></a>希尔排序(Shell Sort)</h3><hr><p>从上面对直接插入排序算法的分析,在最坏情况下其时间复杂度为 $O(n^2)$,在最好情况下其时间复杂度为 $O(n)$,不难推断出:</p><blockquote><p>① 如果<strong>待排序元素基本有序</strong>的话,应用直接插入排序的效率将大大提高。</p></blockquote><p>而显然:</p><blockquote><p>②当<strong>元素个数n较小时</strong>,直接插入排序的效率也较高。</p></blockquote><p>Shell排序正是从这两方面出发对插入排序进行改进而得到的一种效率较高的排序算法。</p><h4 id="基本思想-2"><a href="#基本思想-2" class="headerlink" title="基本思想"></a>基本思想</h4><p>先将整个待排数据元素序列分割成若干子序列,分别对个子序列进行直接插入排序,等整个序列中的数据元素“基本有序”时,再对全体数据元素进行一次直接插入排序。</p><p>设待排序元素序列有 $n$ 个元素,首先取一个整数 $incr<n$ 作为间隔,将全部元素分成 $incr$ 个子序列(位置 0 ~ $incr-1$的元素分属不同子序列,故有 $incr$ 个子序列),所有距离为 $incr$ 的元素放在同一个子序列中,在每一个子序列中分别施行直接插入排序。然后缩小间隔 $incr$,例如 $incr=\frac{incr}{2}$,重复上述子序列划分和排序工作,直至 $incr=1$,即将所有元素都划分至同一个序列中再排序完成为止。</p><p>由于开始时 $incr$ 的取值较大,<strong>每个子序列中的元素较少</strong>,排序速度较快(因为①);而待到排序的后期,$incr$ 取值不断递减,子序列的元素数量逐渐增多,但由于前面工作的基础,<strong>大多数元素已基本有序</strong>,所以排序速度依然很快(因为②)。</p><h4 id="代码实现-2"><a href="#代码实现-2" class="headerlink" title="代码实现"></a>代码实现</h4><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">ShellInsert</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="keyword">int</span> n, <span class="keyword">int</span> incr)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="comment">// 对数组elem作一趟增量为incr的Shell排序</span></span><br><span class="line"> <span class="comment">// 与插入排序的区别在于此处子序列中前后相邻记录的增量为incr,而不是1</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = incr; i < n; i++)</span><br><span class="line"> {</span><br><span class="line"> <span class="comment">// 第i趟插入排序</span></span><br><span class="line"> <span class="keyword">int</span> e = elem[i]; <span class="comment">// 暂存待插入元素</span></span><br><span class="line"> <span class="keyword">int</span> j;</span><br><span class="line"> <span class="keyword">for</span> (j = i - incr; j >= <span class="number">0</span> && e < elem[j]; j -= incr)</span><br><span class="line"> {</span><br><span class="line"> <span class="comment">// 将子序列中比e大的记录都后移,注意是在子序列中后移,下一个位置是j+incr</span></span><br><span class="line"> elem[j + incr] = elem[j];</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 结束上述循环即找到令elem[i] >= elem[j]的位置j</span></span><br><span class="line"> <span class="comment">// 此时elem[i]应该放在elem[j]在同一个子序列中的下一个位置,即j+incr</span></span><br><span class="line"> elem[j + incr] = e;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// t为增量序列长度</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">ShellSort</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="keyword">int</span> n, <span class="built_in">vector</span><<span class="keyword">int</span>> &incrs, <span class="keyword">int</span> t)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> k = <span class="number">0</span>; k < t; k++)</span><br><span class="line"> {</span><br><span class="line"> <span class="comment">// 第k趟希尔排序</span></span><br><span class="line"> ShellInsert(elem, n, incrs[k]);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="复杂度分析-2"><a href="#复杂度分析-2" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><p>分析希尔排序是一个十分复杂的问题,它的时间复杂度是“增量”序列的函数,到现在为止尚未得到数学上的解决。有人利用大量的实验统计资料得出,当 $n$ 很大时,元素的平均比较次数和平均移动次数大于在 $n^{1.25}$ 到 $1.6n^{1.25}$ 的范围内,这是在利用直接插入排序作为子序列排序方法的情况下得到的。还有人指出,当增量序列第 $k$ 个元素 $incrs[k]=2^{t-k+1}-1$ 时,希尔排序的平均时间复杂度为 $O(n^{1.5})$。</p><p>至于增量序列 $incrs$ 应如何选取,则有多种方案。最初Shell提出取 $incr=\frac{n}{2}$,$incr=\frac{incr}{2}$,直到 $incr=1$。但由于直到最后一步奇数位置上的元素才会和偶数位置上的元素进行比较,这样使用这个序列的效率将很低。后来Knuth提出取 $incr=\frac{incr}{3}+1$。还有人提出都取奇数为好,也有人提出各个增量 $incr$ 互质更好。应用不同的增量会对希尔排序算法的性能有很大影响,有些序列的效率会有明显的提高。</p><p><strong>希尔排序是一种不稳定的排序算法。</strong></p><h2 id="交换排序-Exchange-Sort"><a href="#交换排序-Exchange-Sort" class="headerlink" title="交换排序(Exchange Sort)"></a>交换排序(Exchange Sort)</h2><hr><h3 id="冒泡排序-Bubble-Sort"><a href="#冒泡排序-Bubble-Sort" class="headerlink" title="冒泡排序(Bubble Sort)"></a>冒泡排序(Bubble Sort)</h3><hr><p>由于在该排序算法对应的过程中,较小的元素向水中的气泡一样逐渐向上(前)漂浮,较大的元素像石块一样向下(后)沉,并且每一趟比较完成后都有一块最大的“石块”沉到水底,冒泡排序因此得名。</p><h4 id="基本思想-3"><a href="#基本思想-3" class="headerlink" title="基本思想"></a>基本思想</h4><p>将序列的第1个元素与第2个元素进行比较,如前者大于后者,则两个元素交换位置,否则不交换;再将第2个元素与第3个元素进行比较,根据对应大小关系决定是否交换位置…以此类推,直到第 $n-1$ 个元素和第 $n$ 个元素比较(或交换或不交换)。经过如此一趟排序,使得 $n$ 个元素中的最大者被安置在第 $n$ 个位置上(即它的正确位置)。此后,再对前 $n-1$ 个元素进行同样过程,使得该 $n-1$ 个元素的最大者被安置在第 $n-1$ 个位置上(也即它的正确位置)。以此类推,直到某一趟排序过程不出现元素位置交换的动作,即完成第 $n-1$ 趟排序,排序过程结束。</p><h4 id="代码实现-3"><a href="#代码实现-3" class="headerlink" title="代码实现"></a>代码实现</h4><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">BubbleSort</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="keyword">int</span> n)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">1</span>; i < n; i++)</span><br><span class="line"> {</span><br><span class="line"> <span class="comment">// 第i趟冒泡排序,将第i大的元素放在位置n-i上</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> j = <span class="number">0</span>; j < n - i; j++)</span><br><span class="line"> {</span><br><span class="line"> <span class="comment">// 在第i趟冒泡排序过程中需要执行n-i次比较和交换操作</span></span><br><span class="line"> <span class="keyword">if</span> (elem[j] > elem[j + <span class="number">1</span>])</span><br><span class="line"> swap(elem[j], elem[j + <span class="number">1</span>]);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="算法改进"><a href="#算法改进" class="headerlink" title="算法改进"></a>算法改进</h4><p>考虑到具体的一个待排元素序列时可能不需要 $n-1$ 趟冒泡排序就已经排好序了,增加一个标志位 <code>exchange</code> 用于标识此趟($n-1$ 次外循环中的某一次)排序是否发生了逆序和交换。如果没有交换则 <code>exchange</code> 始终为<code>false</code>,表示全部元素已经排好序了,从而可以提前终止处理,结束算法,而无需非要完成全部 $n-1$ 趟排序。如果 <code>exchange</code> 为 <code>true</code>,则表示本趟排序有元素交换发生,还需执行下一趟排序。</p><p>改进后的冒泡排序代码实现如下:<br><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">AdvancedBubbleSort</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="keyword">int</span> n)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">1</span>; i < n; i++)</span><br><span class="line"> {</span><br><span class="line"> <span class="comment">// 第i趟冒泡排序,将第i大的元素放在位置n-i上</span></span><br><span class="line"> <span class="keyword">bool</span> exchange = <span class="literal">false</span>;</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> j = <span class="number">0</span>; j < n - i; j++)</span><br><span class="line"> {</span><br><span class="line"> <span class="comment">// 在第i趟冒泡排序过程中需要执行n-i次比较和交换操作</span></span><br><span class="line"> <span class="keyword">if</span> (elem[j] > elem[j + <span class="number">1</span>])</span><br><span class="line"> {</span><br><span class="line"> swap(elem[j], elem[j + <span class="number">1</span>]);</span><br><span class="line"> exchange = <span class="literal">true</span>; <span class="comment">// 本趟排序发生交换</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (!exchange) <span class="comment">// 本趟排序未发生交换,则排序完成,算法结束</span></span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><h4 id="复杂度分析-3"><a href="#复杂度分析-3" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><p>冒泡排序的外层for循环次数为 $n-1$,内层for的循环次数为 $i$,可知内层循环的元素总比较次数为:</p><center>$$1+2+…+(n-1)=\frac{n(n-1)}{2}=O(n^2)$$</center><p>冒泡排序中,第 $i$ 趟排序需要执行 $n-i$ 次元素比较操作,但不是每次比较都会导致交换(移动)操作。即冒泡排序的数据比较次数与初始序列排列顺序无关,均需要 $\frac{n(n-1)}{2}$ 即 $O(n^2)$ 次操作,但数据交换次数与各待排序元素的初始排列有关,它与逆序的发生有关,最好情况下可能一次都不交换,最差情况下每一次比较操作都会导致元素交换即移动操作,但时间复杂度都是 $O(n^2)$。另外还可证明在平均情况下,冒泡排序的时间复杂度仍为 $O(n^2)$。</p><p><strong>冒泡排序是一种稳定的排序算法。</strong></p><h3 id="快速排序-Quick-Sort"><a href="#快速排序-Quick-Sort" class="headerlink" title="快速排序(Quick Sort)"></a>快速排序(Quick Sort)</h3><hr><p>快速排序平均时间性能最快,有着广泛应用,实际上标准C++类库即STL中的排序程序就被称为 <code>qsort</code> (当然还有 <code>sort</code> ),即默认使用快速排序实现。但值得注意的是,在初始序列有序的情况下,快速排序的时间性能反而最差。</p><h4 id="基本思想-4"><a href="#基本思想-4" class="headerlink" title="基本思想"></a>基本思想</h4><p>任选序列中的一个数据元素,通常选第一个元素,作为<strong>枢轴(pivot)</strong>,用它和所有剩余数据进行比较,将所有比它小的数据元素都排在它之前,将所有比它大的数据元素都排在它之后,经过一趟排序后,可按此数据元素即枢轴元素位置为界,将序列划分为两个部分,并且该枢轴元素已经放在了它应该放置的位置。之后再对这两个部分重复上述步骤,即递归,直至每一个部分中只剩下一个数据元素为止。</p><h4 id="代码实现-4"><a href="#代码实现-4" class="headerlink" title="代码实现"></a>代码实现</h4><p>基于上节介绍的算法思想,有如下快排的递归实现:<br><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">Partition</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="keyword">int</span> low, <span class="keyword">int</span> high)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="comment">// 初始枢轴元素为elem[low]</span></span><br><span class="line"> <span class="comment">// 交换elem中的元素,使枢轴移动到适当位置</span></span><br><span class="line"> <span class="comment">// 要求在枢轴之前的元素不大于枢轴,在枢轴之后的元素不小于枢轴</span></span><br><span class="line"> <span class="comment">// 并返回枢轴位置</span></span><br><span class="line"> <span class="keyword">while</span> (low < high)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">while</span> (low < high && elem[low] <= elem[high]) <span class="comment">// elem[low]为枢轴</span></span><br><span class="line"> high--;</span><br><span class="line"> swap(elem[low], elem[high]);</span><br><span class="line"> <span class="keyword">while</span> (low < high && elem[high] >= elem[low]) <span class="comment">// elem[high]为枢轴</span></span><br><span class="line"> low++;</span><br><span class="line"> swap(elem[low], elem[high]);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> low; <span class="comment">// 返回枢轴位置</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">QuickSortHelp</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="keyword">int</span> low, <span class="keyword">int</span> high)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">if</span> (low < high) <span class="comment">// 有效序列边界</span></span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">int</span> pivot = Partition(elem, low, high);</span><br><span class="line"> QuickSortHelp(elem, low, pivot - <span class="number">1</span>); <span class="comment">// 递归,对子数组elem[low, pivot-1]进行排序</span></span><br><span class="line"> QuickSortHelp(elem, pivot + <span class="number">1</span>, high); <span class="comment">// 递归,对子数组elem[pivot+1, high]进行排序</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">RecursionQuickSort</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="keyword">int</span> n)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> QuickSortHelp(elem, <span class="number">0</span>, n - <span class="number">1</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>能用递归实现的算法,一般都能写出非递归,即迭代实现,这里利用一个<strong>辅助栈</strong>,代码如下:<br><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">Region</span></span></span><br><span class="line"><span class="class">{</span></span><br><span class="line"> <span class="keyword">int</span> low;</span><br><span class="line"> <span class="keyword">int</span> high;</span><br><span class="line">} Region;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">NonRecursionQuickSort</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="keyword">int</span> n)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="built_in">stack</span><Region> regions; <span class="comment">// 定义一个栈变量</span></span><br><span class="line"> Region region;</span><br><span class="line"> region.low = <span class="number">0</span>;</span><br><span class="line"> region.high = n - <span class="number">1</span>;</span><br><span class="line"> regions.push(region);</span><br><span class="line"> <span class="keyword">while</span> (!regions.empty())</span><br><span class="line"> {</span><br><span class="line"> region = regions.top();</span><br><span class="line"> regions.pop();</span><br><span class="line"> <span class="keyword">int</span> pivot = Partition(elem, region.low, region.high);</span><br><span class="line"> <span class="keyword">if</span> (pivot - <span class="number">1</span> > region.low)</span><br><span class="line"> {</span><br><span class="line"> Region regionLow;</span><br><span class="line"> regionLow.low = region.low;</span><br><span class="line"> regionLow.high = pivot - <span class="number">1</span>;</span><br><span class="line"> regions.push(regionLow);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (pivot + <span class="number">1</span> < region.high)</span><br><span class="line"> {</span><br><span class="line"> Region regionHigh;</span><br><span class="line"> regionHigh.low = pivot + <span class="number">1</span>;</span><br><span class="line"> regionHigh.high = region.high;</span><br><span class="line"> regions.push(regionHigh);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><h4 id="复杂度分析-4"><a href="#复杂度分析-4" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><p>快速排序的趟数取决于递归树的深度。如果每次划分即对一个枢轴元素定位后,该元素的左侧和右侧子序列的长度相同,则下一步是将两个长度减半的子序列分别排序,这是最便于估算时间复杂度的理想情况。在 $n$ 个元素中,对一个元素定位所需时间为 $O(n)$,即一次划分 <code>Partition</code> 所用时间。若设 $T(n)$ 为对 $n$ 个元素的序列进行排序所需的时间,而且每次对一个元素正确定位后,正好以此枢轴元素为界将此序列划分为长度相等的两个子序列,此时,总的计算时间为:</p><center>$$T(n) \leq cn + 2T(\frac{n}{2}) = … = cn\log{n}+nT(1) = O(n\log{n})$$</center><p>其中 $c$ 为常数,$cn$ 表示对枢轴元素定位所需时间 $(O(n))$。</p><p>可以证明,快排的平均时间复杂度也是 $O(n\log{n})$。此外,实验结果也表明,快速排序在我们所讨论的所有内部排序算法中是平均性能最好的一个。由于快速排序是递归的,需要有一个栈存放每层递归调用时的指针和参数,最大递归调用层数与递归树的深度一致,理想情况下为 $\lceil\log{(n+1)}\rceil$,即存储空间开销为 $O(\log{n})$。</p><p>然而,在数据原本有序的情况下,由于我们每次选择第一个元素作为枢轴元素,这样得到的递归树将成为无分叉的单支树,每次划分只会得到一个比上次少一个元素的子序列,这样就必须经过 $n-1$ 趟才能把所有元素定位,而且第1趟需要经过 $n-1$ 次比较才能找到第1个元素的安放位置,第2趟需要经过 $n-2$ 次比较才能找到第2个元素的安放位置,…,总的比较次数将达到:</p><center>$$\sum_{i=1}^{n-1} (n-i)=\frac{n(n-1)}{2}\approx\frac{n^2}{2} = O(n^2)$$</center><p>即排序速度退化成冒泡排序,比直接插入排序都慢,且占用附加存储空间(栈)将达到 $O(n)$。</p><p><strong>快速排序是一种不稳定的排序算法。</strong></p><h2 id="选择排序-Selection-Sort"><a href="#选择排序-Selection-Sort" class="headerlink" title="选择排序(Selection Sort)"></a>选择排序(Selection Sort)</h2><hr><h3 id="简单选择排序-Simple-Selection-Sort"><a href="#简单选择排序-Simple-Selection-Sort" class="headerlink" title="简单选择排序(Simple Selection Sort)"></a>简单选择排序(Simple Selection Sort)</h3><hr><h4 id="基本思想-5"><a href="#基本思想-5" class="headerlink" title="基本思想"></a>基本思想</h4><p>每一趟在 $n-i (i=1,2,…,n-1)$ 个数据元素($elem[i],elem[i+1],…,elem[n-1]$)中选择最小数据元素作为有序序列的第 $i$ 个数据元素。</p><p>简单选择排序的第 $i$ 趟是从 $elem[i],…,elem[n-1]$ 中选择整个序列中第 $i$ 小的元素,并将此元素放在 $elem[i]$ 处,也就是说简单选择排序是从未排序的序列中选择最小元素,接着是次小的,…,第 $i$ 小的,…,依此类推,为寻找下一个最小元素,需检索数据整个的未排序部分,即从 $elem[i]$ 开始一直到 $elem[n-1]$ 结束的这部分序列,但每趟排序都只用交换一次元素位置。即把此趟排序找出的最小元素放置到正确位置(当前排序序列的第一个位置,即 $elem[i]$),直至倒数第二小的被放到数组的倒数第二个位置,则整个序列排序完成。</p><h4 id="代码实现-5"><a href="#代码实现-5" class="headerlink" title="代码实现"></a>代码实现</h4><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">SimpleSelectionSort</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="keyword">int</span> n)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i < n - <span class="number">1</span>; i++) <span class="comment">// 共需n-1趟排序</span></span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">int</span> minIndex = i; <span class="comment">// 第i趟排序先访问第i个元素</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> j = i + <span class="number">1</span>; j < n; j++) <span class="comment">// 上面已经访问第i个元素,这里从i+1开始遍历此后的子序列</span></span><br><span class="line"> <span class="keyword">if</span> (elem[j] < elem[minIndex])</span><br><span class="line"> minIndex = j;</span><br><span class="line"> swap(elem[i], elem[minIndex]); <span class="comment">// 将第i趟排序的最小元素放置到第i个位置elem[i]处</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="复杂度分析-5"><a href="#复杂度分析-5" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><p>简单选择排序的外层 $for$ 循环共循环 $n-1$ 次,内层 $for$ 循环共循环 $n-1-i$ 次,可知元素总比较次数为:</p><center>$$\sum_{i=0}^{n-2}(n-1-i) = \frac{n(n-1)}{2} = O(n^2)$$</center><!-- <blockquote class="blockquote-center">居中的引用文本,会出现左右两个大引号</blockquote> --><p>而元素移动只在每趟外层 $for$ 末尾发生一次,故仅需 $n-1$ 次,即 $O(n)$,故简单选择排序的时间复杂度为:</p><center>$$O(n^2) + O(n) = O(n^2)$$</center><p><strong>简单选择排序是一种不稳定的排序算法。</strong></p><h3 id="堆排序-Heap-Sort"><a href="#堆排序-Heap-Sort" class="headerlink" title="堆排序(Heap Sort)"></a>堆排序(Heap Sort)</h3><hr><h4 id="基本思想-6"><a href="#基本思想-6" class="headerlink" title="基本思想"></a>基本思想</h4><p>堆排序也是一种给予选择排序的先进排序方法,只需要一个元素的辅助存储空间,关于堆这种数据结构的详细介绍请参见<a href="http://dongdongdong.me/2018/05/03/Algorithm/DataStructure/stack-queue-priority_queue-heap/"><strong>此篇</strong></a>。在堆排序中我们会用到最大堆,这里你只需知道它实际是个二叉树,每个非叶结点的关键码都比其子结点的关键码要大,根结点的关键码最大,每次插入或删除结点都可能会破坏符合上述定义的堆结构,因此需要重新调整使其再度成为堆。</p><p>堆排序的思想其实不难想到:先将一个无序序列构造成一个最大堆,再将堆顶元素(即最大元素)与末尾元素交换位置,即最大元素被放到序列末尾位置,那么该元素就已经放置到其正确位置,此后调整除去末尾元素的子序列使其重新成为最大堆。重复上述过程,每次调整成堆之后都会将一个堆顶元素,即当前排序序列中最大的元素放置到当前序列中的最后一个位置,即其正确位置上,这样需要经过n-1次调整最终会将所有元素放置到正确位置,排序完成。</p><p>当一个含有 $n$ 个结点的二叉树在数组中存储时,你需要知道相关结点的下标计算方法如下:</p><ul><li>二叉树第一个非叶结点下标:$\frac{n-2}{2}$</li><li>下标为$i$的结点的父结点下标:$\frac{n-1}{2}$</li><li>下标为$i$的结点的孩子结点下标:左孩子为 $2i+1$,右孩子为 $2i+2$</li></ul><h4 id="代码实现-6"><a href="#代码实现-6" class="headerlink" title="代码实现"></a>代码实现</h4><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">SiftAdjust</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="keyword">int</span> low, <span class="keyword">int</span> high)</span> </span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="comment">// 自上而下调整low元素位置使其符合最大堆定义,此过程又称“筛选”,在堆方法中一般命名为SiftDown</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = low, j = <span class="number">2</span> * low + <span class="number">1</span>; j <= high; j = <span class="number">2</span> * j + <span class="number">1</span>)</span><br><span class="line"> {</span><br><span class="line"> <span class="comment">// i为被调整结点,j为其最大孩子,初始是左孩子2i+1,右孩子是2i+2</span></span><br><span class="line"> <span class="keyword">if</span> (j < high && elem[j] < elem[j + <span class="number">1</span>]) <span class="comment">// j指向i的最大孩子</span></span><br><span class="line"> j++;</span><br><span class="line"> <span class="keyword">if</span> (elem[i] > elem[j]) <span class="comment">// 已经成为最大堆,即父结点关键码大于最大子结点关键码</span></span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> swap(elem[i], elem[j]); <span class="comment">// 当前结点下沉,其父结点上浮</span></span><br><span class="line"> i = j; <span class="comment">// 当前要调整的结点下降</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">HeapSort</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="keyword">int</span> n)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">int</span> i;</span><br><span class="line"> <span class="comment">// 将待排序元素数组构造成堆,从第一个非叶结点开始调整,直到根结点</span></span><br><span class="line"> <span class="keyword">for</span> (i = (n - <span class="number">2</span>) / <span class="number">2</span>; i >= <span class="number">0</span>; i--) <span class="comment">// 第一个非叶结点下标为(n-2)/2</span></span><br><span class="line"> SiftAdjust(elem, i, n - <span class="number">1</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> (i = n - <span class="number">1</span>; i > <span class="number">0</span>; i--)</span><br><span class="line"> {</span><br><span class="line"> <span class="comment">// 第i趟堆排序</span></span><br><span class="line"> <span class="comment">// 将当前堆顶元素(最大元素)放到当前序列末尾,即第i个位置即其正确位置</span></span><br><span class="line"> swap(elem[<span class="number">0</span>], elem[i]); <span class="comment">// 交换当前堆顶与末尾元素</span></span><br><span class="line"> SiftAdjust(elem, <span class="number">0</span>, i - <span class="number">1</span>); <span class="comment">// 调整从elem[0...i-1]的子序列成为最大堆</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="复杂度分析-6"><a href="#复杂度分析-6" class="headerlink" title="复杂度分析"></a>复杂度分析</h4><p>若设堆中有 $n$ 个结点,且树的深度为 $k$,则有 $2^{k-1} \leq n < 2^k$,对应的完全二叉树有 $k$ 层。在第 $i$ 层上的结点数 $\leq$ $2^{i-1}$。在第一个for循环中构造初始堆过程中,对每一个非叶结点都调用了一次堆调整算法 <code>SiftAdjust()</code>,因此该循环计算时间为:</p><center>$$2 \cdot \sum_{i=1}^{k-1} 2^{i-1} \cdot (k-i)$$</center><p>其中,$i$ 是层次编号,$2^{i-1}$ 是第i层的最大结点数,$(k-i)$ 是第i层结点所能移动的最大距离(从上往下调整的纵向距离)。</p><p>设 $j=k-i$,则有:</p><center>$$2 \cdot \sum_{i=1}^{k-1} 2^{i-1} \cdot (k-i)$$</center><center>$$=2 \cdot \sum_{j=1}^{k-1} 2^{k-j-1} \cdot j$$</center><center>$$=2 \cdot 2^{k-1} \sum_{j=1}^{k-1}\frac{j}{2^j}$$</center><center>$$< 2 \cdot n\sum_{j=1}^{k-1}\frac{j}{2^j}$$</center><center>$$< 4n = O(n)$$</center><p>在第二个 $for$ 循环中,调用了 $n-1$ 次 <code>SiftAdjust()</code> 方法,由<a href="http://dongdongdong.me/2018/05/03/Algorithm/DataStructure/stack-queue-priority_queue-heap/"><strong>此篇</strong></a>可知该循环的总计算时间为 $O(n\log{n})$,因此堆排序的时间复杂度为 $O(n\log{n})$。该算法的附件存储主要是在第二个 $for$ 循环中用来执行元素交换时所用的一个临时元素,即空间复杂度为 $O(1)$。</p><p><strong>堆排序是一种不稳定的排序算法。</strong></p><h2 id="归并排序-Merge-Sort"><a href="#归并排序-Merge-Sort" class="headerlink" title="归并排序(Merge Sort)"></a>归并排序(Merge Sort)</h2><hr><h3 id="基本思想-7"><a href="#基本思想-7" class="headerlink" title="基本思想"></a>基本思想</h3><p>归并是指将两个有序的子序列合并和一个新的有序子序列,设在初始序列中有 $n$ 个元素,归并排序的基本思想是:将序列看成 $n$ 个有序的子序列,每个序列长度为1,然后两两归并,得到 $\lceil\frac{n}{2}\rceil$ 个长度为2或1的有序子序列,然后再两两归并,…,这样重复下去,直至得到一个长度为 $n$ 的有序子序列,这种排序方法称为 <strong>2-路归并排序</strong>。如果每次将3个有序子序列合并为一个新的有序子序列,则称为 <strong>3-路归并排序</strong>,并可依次类推。对于<strong>内部排序</strong>而言,2-路归并排序就能完全满足实现需要,只有<strong>外部排序</strong>才需要多路归并以减少磁盘IO次数,本节仅讨论2-路归并排序。</p><h3 id="代码实现-7"><a href="#代码实现-7" class="headerlink" title="代码实现"></a>代码实现</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">RecursionSimplenMerge</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="keyword">int</span> low, <span class="keyword">int</span> mid, <span class="keyword">int</span> high)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="comment">// 将有序子序列elem[low...mid]和elem[mid+1...high]归并为新的有序子序列elem[low...high]</span></span><br><span class="line"> <span class="keyword">int</span> *tmpElem = <span class="keyword">new</span> <span class="keyword">int</span>[high + <span class="number">1</span>]; <span class="comment">// 临时数组用于存储结果数组</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">int</span> i, j, k; <span class="comment">// 分别表示数组elem[low...mid]、elem[mid+1...high]和结果数组tmpElem中当前元素的下标</span></span><br><span class="line"> <span class="keyword">for</span> (i = low, j = mid + <span class="number">1</span>, k = low; i <= mid && j <= high; k++)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">if</span> (elem[i] <= elem[j])</span><br><span class="line"> {</span><br><span class="line"> <span class="comment">// elem[i]较小,先放入结果数组tmpElem</span></span><br><span class="line"> tmpElem[k] = elem[i];</span><br><span class="line"> i++;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> {</span><br><span class="line"> <span class="comment">// elem[j]较小,先放入结果数组tmpElem</span></span><br><span class="line"> tmpElem[k] = elem[j];</span><br><span class="line"> j++;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> (; i <= mid; i++, k++)</span><br><span class="line"> tmpElem[k] = elem[i];</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> (; j <= high; j++, k++)</span><br><span class="line"> tmpElem[k] = elem[j];</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> (i = low; i <= high; i++)</span><br><span class="line"> <span class="comment">// 将结果tmpElem[low...high]复制到原数组elem[low...high]</span></span><br><span class="line"> elem[i] = tmpElem[i];</span><br><span class="line"></span><br><span class="line"> <span class="keyword">delete</span>[] tmpElem; <span class="comment">// 一定记得new出来的空间要手动delete释放</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">RecursionSimpleMergeSortHelp</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="keyword">int</span> low, <span class="keyword">int</span> high)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="comment">// 划分区间,合并区间,即完成归并排序</span></span><br><span class="line"> <span class="keyword">if</span> (low < high) <span class="comment">// low = high 即区间只有一个元素时结束循环,无需再划分</span></span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">int</span> mid = (low + high) / <span class="number">2</span>; <span class="comment">// 划分区间 low~mid 和 mid+1~high</span></span><br><span class="line"> RecursionSimpleMergeSortHelp(elem, low, mid); <span class="comment">// 对elem[low...mid]进行归并排序</span></span><br><span class="line"> RecursionSimpleMergeSortHelp(elem, mid + <span class="number">1</span>, high); <span class="comment">// 对elem[mid+1...high]进行归并排序</span></span><br><span class="line"> RecursionSimpleMerge(elem, low, mid, high); <span class="comment">// 堆elem[low...mid]和elem[mid+1...high]两个区间排序结果进行归并</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">RecursionSimpleMergeSort</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="keyword">int</span> n)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> SimpleMergeSortHelp(elem, <span class="number">0</span>, n - <span class="number">1</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>对于上面的归并排序实现,在每次归并时都要为临时 $vector$ 分配存储空间,归并结束后还需释放空间,要花费不少时间,为进一步提高运行速度,可在主归并函数 <code>RecursionMergeSort()</code> 中统一为临时 $vector$ 分配存储空间,为作区分,以上代码实现的归并排序算法称之为<strong>简单归并排序</strong>,方法名为 <code>RecursionSimpleMergeSort()</code> ,下面经改进过的归并排序方法名为 <code>RecursionMergeSort()</code>。<br><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">RecursionMerge</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="built_in">vector</span><<span class="keyword">int</span>> &tmpElem, <span class="keyword">int</span> low, <span class="keyword">int</span> mid, <span class="keyword">int</span> high)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="comment">// 将有序子序列elem[low...mid]和elem[mid+1...high]归并为新的有序子序列elem[low...high]</span></span><br><span class="line"> <span class="keyword">int</span> i, j, k; <span class="comment">// 分别表示数组elem[low...mid]、elem[mid+1...high]和结果数组tmpElem中当前元素的下标</span></span><br><span class="line"> <span class="keyword">for</span> (i = low, j = mid + <span class="number">1</span>, k = low; i <= mid && j <= high; k++)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">if</span> (elem[i] <= elem[j])</span><br><span class="line"> {</span><br><span class="line"> <span class="comment">// elem[i]较小,先放入结果数组tmpElem</span></span><br><span class="line"> tmpElem[k] = elem[i];</span><br><span class="line"> i++;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> {</span><br><span class="line"> <span class="comment">// elem[j]较小,先放入结果数组tmpElem</span></span><br><span class="line"> tmpElem[k] = elem[j];</span><br><span class="line"> j++;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> (; i <= mid; i++, k++)</span><br><span class="line"> tmpElem[k] = elem[i];</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> (; j <= high; j++, k++)</span><br><span class="line"> tmpElem[k] = elem[j];</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> (i = low; i <= high; i++)</span><br><span class="line"> <span class="comment">// 将结果tmpElem[low...high]复制到原数组elem[low...high]</span></span><br><span class="line"> elem[i] = tmpElem[i];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">RecursionMergeSortHelp</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="built_in">vector</span><<span class="keyword">int</span>> &tmpElem, <span class="keyword">int</span> low, <span class="keyword">int</span> high)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="comment">// 划分区间,合并区间,即完成归并排序</span></span><br><span class="line"> <span class="keyword">if</span> (low < high) <span class="comment">// low = high 即区间只有一个元素时结束循环,无需再划分</span></span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">int</span> mid = (low + high) / <span class="number">2</span>; <span class="comment">// 划分区间 low~mid 和 mid+1~high</span></span><br><span class="line"> MergeSortHelp(elem, tmpElem, low, mid); <span class="comment">// 对elem[low...mid]进行归并排序</span></span><br><span class="line"> MergeSortHelp(elem, tmpElem, mid + <span class="number">1</span>, high); <span class="comment">// 对elem[mid+1...high]进行归并排序</span></span><br><span class="line"> Merge(elem, tmpElem, low, mid, high); <span class="comment">// 堆elem[low...mid]和elem[mid+1...high]两个区间排序结果进行归并</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">RecursionMergeSort</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="keyword">int</span> n)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="built_in">vector</span><<span class="keyword">int</span>> tmpElem(n, <span class="number">0</span>);</span><br><span class="line"> MergeSortHelp(elem, tmpElem, <span class="number">0</span>, n - <span class="number">1</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>另外给出一种形式更简洁的递归版归并排序实现:<br><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">RecursionMergeSort</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="keyword">int</span> start, <span class="keyword">int</span> end)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="comment">// 当子序列就只有一个元素的时候就弹出</span></span><br><span class="line"> <span class="keyword">if</span> (start == end)</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 分治</span></span><br><span class="line"> <span class="keyword">int</span> mid = (start + end) / <span class="number">2</span>;</span><br><span class="line"> MergeSort(elem, start, mid);</span><br><span class="line"> MergeSort(elem, mid + <span class="number">1</span>, end);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 合并</span></span><br><span class="line"> Merge(elem, start, mid, end);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">Merge</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="keyword">int</span> start, <span class="keyword">int</span> mid, <span class="keyword">int</span> end)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="built_in">vector</span><<span class="keyword">int</span>> tmpElem(end - start + <span class="number">1</span>, <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">int</span> i = start, j = mid + <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> k = start; k <= end; k++)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">if</span> (i > mid)</span><br><span class="line"> <span class="comment">// 第一个序列元素已合并完</span></span><br><span class="line"> tmpElem[k] = elem[j++];</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (j > end)</span><br><span class="line"> <span class="comment">// 第二个序列元素已合并完</span></span><br><span class="line"> tmpElem[k] = elem[i++];</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (elem[i] > elem[j])</span><br><span class="line"> <span class="comment">// 先放较小的元素</span></span><br><span class="line"> tmpElem[k] = elem[j++];</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> <span class="comment">// 先放较小的元素</span></span><br><span class="line"> tmpElem[k] = elem[i++];</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = start; i <= end; i++)</span><br><span class="line"> <span class="comment">// 元素放回原数组</span></span><br><span class="line"> elem[i] = tmpElem[i];</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>与快排一样,能用递归实现的算法,一般都能写出非递归,即迭代实现,思想和递归正好相反,原来的递归过程是将待排序集合一分为二,此后不断划分直至排序集合就剩下一个元素位置,然后不断的合并两个排好序的数组。</p><p>所以非递归版本的归并排序的思想为:将数组中的相邻元素两两配对。用 <code>Merge()</code> 函数将它们排序,构成 $\frac{n}{2}$ 组长度为2的排序好的子数组段,然后再将它们排序成长度为4的子数组段,如此继续下去,直至整个数组排好序,代码如下:<br><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">Merge</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="keyword">int</span> start, <span class="keyword">int</span> mid, <span class="keyword">int</span> end)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="built_in">vector</span><<span class="keyword">int</span>> tmpElem(end - start + <span class="number">1</span>, <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">int</span> i = start, j = mid + <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> k = start; k <= end; k++)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">if</span> (i > mid)</span><br><span class="line"> <span class="comment">// 第一个序列元素已合并完</span></span><br><span class="line"> tmpElem[k] = elem[j++];</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (j > end)</span><br><span class="line"> <span class="comment">// 第二个序列元素已合并完</span></span><br><span class="line"> tmpElem[k] = elem[i++];</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (elem[i] > elem[j])</span><br><span class="line"> <span class="comment">// 先放较小的元素</span></span><br><span class="line"> tmpElem[k] = elem[j++];</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> <span class="comment">// 先放较小的元素</span></span><br><span class="line"> tmpElem[k] = elem[i++];</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = start; i <= end; i++)</span><br><span class="line"> <span class="comment">// 元素放回原数组</span></span><br><span class="line"> elem[i] = tmpElem[i];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">NonRecursionMergeSort</span><span class="params">(<span class="built_in">vector</span><<span class="keyword">int</span>> &elem, <span class="keyword">int</span> n)</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="keyword">int</span> s = <span class="number">2</span>, i;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">while</span> (s <= n)</span><br><span class="line"> {</span><br><span class="line"> i = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">while</span> (i + s <= n)</span><br><span class="line"> {</span><br><span class="line"> Merge(elem, i, i + s / <span class="number">2</span> - <span class="number">1</span>, i + s - <span class="number">1</span>);</span><br><span class="line"> i += s;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">//处理末尾残余部分</span></span><br><span class="line"> Merge(elem, i, i + s / <span class="number">2</span> - <span class="number">1</span>, n - <span class="number">1</span>);</span><br><span class="line"> s *= <span class="number">2</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">//最后再从头到尾处理一遍</span></span><br><span class="line"> Merge(elem, <span class="number">0</span>, s / <span class="number">2</span> - <span class="number">1</span>, n - <span class="number">1</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><h3 id="复杂度分析-7"><a href="#复杂度分析-7" class="headerlink" title="复杂度分析"></a>复杂度分析</h3><p>由于将有序子序列 $elem[low…mid]$ 和 $elem[mid+1…high]$ 归并为有序子序列 $elem[low…high]$ 需要时间 $O(high-low+1)$,可知进行一趟归并需要时间 $O(n)$,设:</p><center>$$2^{h-1} < n \leq 2^h$$</center><p>在进行第1趟归并后,将得到 $n_1=\lceil\frac{n}{2}\rceil$ 个长度为2或更短的有序子序列,其中 $n_1$ 满足:</p><center>$$2^{h-2} \leq n_1 \leq 2^{h-1}$$</center><p>在进行第2趟归并后,将得到 $n_2=\lceil\frac{n_1}{2}\rceil$ 个长度为4或更短的有序子序列,其中 $n_2$ 满足:</p><center>$$2^{h-3} \leq n_2 \leq 2^{h-2}$$</center><p>这样继续下去,在进行第 $h$ 趟归并后,将得到 $n<em>h=\lceil\frac{n</em>{h-1}}{2}\rceil$ 个长度为 $2^h$ 或更短的有序子序列,其中 $n_h$ 满足:</p><center>$$2^{h-(h+1)} \leq n_h \leq 2^{h-h}$$</center><p>由上式可知,$\frac{1}{2} \leq n_h \leq 1$,左移 $n_h=1$,也就是归并躺数为 $h$,再回到开头的式子:</p><center>$$2^{h-1} < n \leq 2^h$$</center><p>上式各项取以2为底的对数可得:</p><center>$$h-1 < \log{n} \leq h$$</center><p>从而 $h=\lceil log{n}\rceil$,这样时间复杂度就为 $O(n\lceil\log{n}\rceil)=O(n\log{n})$。</p><p>并且,归并排序的时间代价并不依赖于待排序数组的初始排列情况,也就是说归并排序的最好、平均、最差情况的时间复杂度都是 $O(n\log{n})$,这一点比快速排序好,当然在平均情况下,还是快速排序最快(常数因子更小)。</p><p><strong>归并排序是一种稳定的排序算法。</strong></p><h2 id="性能对比"><a href="#性能对比" class="headerlink" title="性能对比"></a>性能对比</h2><hr><table><thead><tr><th style="text-align:center">排序算法</th><th style="text-align:center">最好时间</th><th style="text-align:center">平均时间</th><th style="text-align:center">最坏时间</th><th style="text-align:center">辅助空间</th><th style="text-align:center">稳定性</th></tr></thead><tbody><tr><td style="text-align:center">直接插入排序</td><td style="text-align:center">$O(n)$</td><td style="text-align:center">$O(n^2)$</td><td style="text-align:center">$O(n^2)$</td><td style="text-align:center">$O(1)$</td><td style="text-align:center">稳定</td></tr><tr><td style="text-align:center">折半插入排序</td><td style="text-align:center">$O(n\log{n})$</td><td style="text-align:center">$O(n^2)$</td><td style="text-align:center">$O(n^2)$</td><td style="text-align:center">$O(1)$</td><td style="text-align:center">稳定</td></tr><tr><td style="text-align:center">希尔排序</td><td style="text-align:center">由增量序列决定</td><td style="text-align:center">由增量序列决定</td><td style="text-align:center">由增量序列决定</td><td style="text-align:center">$O(1)$</td><td style="text-align:center">不稳定</td></tr><tr><td style="text-align:center">冒泡排序</td><td style="text-align:center">$O(n)$</td><td style="text-align:center">$O(n^2)$</td><td style="text-align:center">$O(n^2)$</td><td style="text-align:center">$O(1)$</td><td style="text-align:center">稳定</td></tr><tr><td style="text-align:center">快速排序</td><td style="text-align:center">$O(n\log{n})$</td><td style="text-align:center">$O(n\log{n})$</td><td style="text-align:center">$O(n^2)$</td><td style="text-align:center">$O(\log{n})/O(n)$</td><td style="text-align:center">不稳定</td></tr><tr><td style="text-align:center">选择排序</td><td style="text-align:center">$O(n^2)$</td><td style="text-align:center">$O(n^2)$</td><td style="text-align:center">$O(n^2)$</td><td style="text-align:center">$O(1)$</td><td style="text-align:center">不稳定</td></tr><tr><td style="text-align:center">堆排序</td><td style="text-align:center">$O(n\log{n})$</td><td style="text-align:center">$O(n\log{n})$</td><td style="text-align:center">$O(n\log{n})$</td><td style="text-align:center">$O(1)$</td><td style="text-align:center">不稳定</td></tr><tr><td style="text-align:center">归并排序</td><td style="text-align:center">$O(n\log{n})$</td><td style="text-align:center">$O(n\log{n})$</td><td style="text-align:center">$O(n\log{n})$</td><td style="text-align:center">$O(n)$</td><td style="text-align:center">稳定</td></tr></tbody></table><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><hr><p>[1]<a href="https://www.amazon.cn/dp/B001TREOXM" target="_blank" rel="noopener">数据结构与算法(C++版) - 唐宁九主编</a><br>[2]<a href="https://www.amazon.cn/dp/B0011F7UHO" target="_blank" rel="noopener">数据结构(用面向对象方法与C++语言描述) - 殷人昆主编</a><br>[3]<a href="http://www.cnblogs.com/onepixel/p/7674659.html" target="_blank" rel="noopener">十大经典排序算法(带动图演示)</a><br>[4]<a href="https://blog.csdn.net/yunzhongguwu005/article/details/9455991" target="_blank" rel="noopener">快速排序详解 - 递归与非递归实现</a><br>[5]<a href="https://blog.csdn.net/lpjishu/article/details/51290930" target="_blank" rel="noopener">归并排序详解 - 递归与非递归实现</a></p>]]></content>
<categories>
<category> Algorithm </category>
</categories>
<tags>
<tag> Algorithm </tag>
<tag> Sort </tag>
</tags>
</entry>
<entry>
<title>数据结构之线性表</title>
<link href="/2018/04/11/Algorithm/DataStructure/linear-list/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><hr><p>线性表实现有两种方式,一种为<strong>顺序表</strong>,另一种为<strong>链表</strong>。本文分别介绍了<strong>顺序线性表</strong>、<strong>单向链表</strong>、<strong>双向链表</strong>和<strong>循环链表</strong>的基本结构,并给出了相应的C++类代码实现。<br><a id="more"></a></p><h2 id="线性表-Linear-List"><a href="#线性表-Linear-List" class="headerlink" title="线性表(Linear List)"></a>线性表(Linear List)</h2><hr><h3 id="顺序表-Sequential-List"><a href="#顺序表-Sequential-List" class="headerlink" title="顺序表(Sequential List)"></a>顺序表(Sequential List)</h3><hr><p>在顺序实现中,数据存储在一个长度为<code>maxSize</code>,数据类型为<code>ElemType</code>的<strong>数组</strong>中,并用<code>count</code>记录在数组中的线性表的实际元素个数。由于顺序表本质是个数组,故其中逻辑位置连续的结点,其物理存储空间也连续。</p><p>顺序表的类声明及定义如下:</p><table><thead><tr><th style="text-align:center">Header</th><th style="text-align:center">Implementation</th></tr></thead><tbody><tr><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/SqList.h" target="_blank" rel="noopener"><strong>SqList.h</strong></a></td><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/SqList.cc" target="_blank" rel="noopener"><strong>SqList.cc</strong></a></td></tr></tbody></table><h3 id="单链表-Linked-List"><a href="#单链表-Linked-List" class="headerlink" title="单链表(Linked List)"></a>单链表(Linked List)</h3><hr><p>单链表是一种最简单的线性表的链式存储结构,单链表也称为线性链表,用它来存储线性表时,每个数据元素用一个结点(<code>node</code>)来存储,一个结点由两个域组成,一个是存放数据元素的<code>val</code>,称为<strong>数据域</strong>,一个是存储指向此链表下一个结点的指针<code>next</code>,称为<strong>指针域</strong>。</p><p>单链表结点的类声明及定义如下:</p><table><thead><tr><th style="text-align:center">Header</th><th style="text-align:center">Implementation</th></tr></thead><tbody><tr><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/Node.h" target="_blank" rel="noopener"><strong>Node.h</strong></a></td><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/Node.cc" target="_blank" rel="noopener"><strong>Node.cc</strong></a></td></tr></tbody></table><p>单链表在表头通常会增加一个没有存储数据元素的结点,称之为”<strong>头结点</strong>“,在单链表中增加头结点虽然增加了存储空间,但算法实现更简单,效率更高。头结点的地址可从指针<code>head</code>找到,指针<code>head</code>也称为”<strong>头指针</strong>“,其它结点的地址则由其前驱结点的<code>next</code>域得到。</p><p>单链表用结点中的指针域来表示数据元素之间的逻辑关系,这样逻辑上相邻的两个元素并不要求物理存储位置也相邻。即在链表中,逻辑位置连续的结点,物理存储空间不必连续</p><h4 id="简单线性链表-Simple-Linked-List"><a href="#简单线性链表-Simple-Linked-List" class="headerlink" title="简单线性链表(Simple Linked List)"></a>简单线性链表(Simple Linked List)</h4><p>线性链表简单实现为数据成员只有<strong>头指针</strong>。</p><p>简单线性链表的类声明及定义如下:</p><table><thead><tr><th style="text-align:center">Header</th><th style="text-align:center">Implementation</th></tr></thead><tbody><tr><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/SimpleLinkList.h" target="_blank" rel="noopener"><strong>SimpleLinkList.h</strong></a></td><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/SimpleLinkList.cc" target="_blank" rel="noopener"><strong>SimpleLinkList.cc</strong></a></td></tr></tbody></table><h4 id="线性链表-Link-List"><a href="#线性链表-Link-List" class="headerlink" title="线性链表(Link List)"></a>线性链表(Link List)</h4><p>单链表简单实现基础上增加了表示当前位置的序号<code>curPosition</code>,指向当前位置的指针<code>curPtr</code>,以及元素总个数<code>count</code>。</p><p>线性链表的类声明及定义如下:</p><table><thead><tr><th style="text-align:center">Header</th><th style="text-align:center">Implementation</th></tr></thead><tbody><tr><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/LinkList.h" target="_blank" rel="noopener"><strong>LinkList.h</strong></a></td><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/LinkList.cc" target="_blank" rel="noopener"><strong>LinkList.cc</strong></a></td></tr></tbody></table><h3 id="双向链表-Double-Linked-List"><a href="#双向链表-Double-Linked-List" class="headerlink" title="双向链表(Double Linked List)"></a>双向链表(Double Linked List)</h3><hr><p>前面介绍的单链表的结点结构中只有一个指向后继的指针域,即<code>next</code>,这样便只能从左往右进行查找其它结点,如要查找前驱,则只有从表头出发进行查找,效率较低,双向链表通过在其结点结构中存储两个指针域<code>back</code>和<code>next</code>,分别指向该结点前驱和后继。</p><p>双向链表结点的类声明及定义如下:</p><table><thead><tr><th style="text-align:center">Header</th><th style="text-align:center">Implementation</th></tr></thead><tbody><tr><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/DblNode.h" target="_blank" rel="noopener"><strong>DblNode.h</strong></a></td><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/DblNode.cc" target="_blank" rel="noopener"><strong>DblNode.cc</strong></a></td></tr></tbody></table><h4 id="简单双向链表-Simple-Double-Linked-List"><a href="#简单双向链表-Simple-Double-Linked-List" class="headerlink" title="简单双向链表(Simple Double Linked List)"></a>简单双向链表(Simple Double Linked List)</h4><p>简单双向链表的类声明及定义如下:</p><table><thead><tr><th style="text-align:center">Header</th><th style="text-align:center">Implementation</th></tr></thead><tbody><tr><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/SimpleDbLinkList.h" target="_blank" rel="noopener"><strong>SimpleDbLinkList.h</strong></a></td><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/SimpleDbLinkList.cc" target="_blank" rel="noopener"><strong>SimpleDbLinkList.cc</strong></a></td></tr></tbody></table><h4 id="双向链表-Double-Linked-List-1"><a href="#双向链表-Double-Linked-List-1" class="headerlink" title="双向链表(Double Linked List)"></a>双向链表(Double Linked List)</h4><p>在双向链表简单实现基础上增加了表示当前位置的序号<code>curPosition</code>,指向当前位置的指针<code>curPtr</code>,以及元素总个数<code>count</code>。</p><p>双向链表的类声明及定义如下:</p><table><thead><tr><th style="text-align:center">Header</th><th style="text-align:center">Implementation</th></tr></thead><tbody><tr><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/DbLinkList.h" target="_blank" rel="noopener"><strong>DbLinkList.h</strong></a></td><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/DbLinkList.cc" target="_blank" rel="noopener"><strong>DbLinkList.cc</strong></a></td></tr></tbody></table><h3 id="循环链表-Circular-Linked-List"><a href="#循环链表-Circular-Linked-List" class="headerlink" title="循环链表(Circular Linked List)"></a>循环链表(Circular Linked List)</h3><hr><p>循环链表是另一种形式的线性表链式存储结构,它的结点结构与普通单链表相同,不同的是在循环链表中尾结点的<code>next</code>域不为空,而是指向起头结点,这样就将单链表首尾相接成为一个环。故而,循环链表判空的条件为:<code>head->next == head</code>。</p><h4 id="简单循环链表-Simple-Circular-Linked-List"><a href="#简单循环链表-Simple-Circular-Linked-List" class="headerlink" title="简单循环链表(Simple Circular Linked List)"></a>简单循环链表(Simple Circular Linked List)</h4><p>简单循环链表的类声明及定义如下:</p><table><thead><tr><th style="text-align:center">Header</th><th style="text-align:center">Implementation</th></tr></thead><tbody><tr><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/SimpleCircLinkList.h" target="_blank" rel="noopener"><strong>SimpleCircLinkList.h</strong></a></td><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/SimpleCircLinkList.cc" target="_blank" rel="noopener"><strong>SimpleCircLinkList.cc</strong></a></td></tr></tbody></table><h4 id="循环链表-Circur-Linked-List"><a href="#循环链表-Circur-Linked-List" class="headerlink" title="循环链表(Circur Linked List)"></a>循环链表(Circur Linked List)</h4><p>在循环链表简单实现基础上增加了表示当前位置的序号<code>curPosition</code>,指向当前位置的指针<code>curPtr</code>,以及元素总个数<code>count</code>。</p><p>循环链表的类声明及定义如下:</p><table><thead><tr><th style="text-align:center">Header</th><th style="text-align:center">Implementation</th></tr></thead><tbody><tr><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/CircLinkList.h" target="_blank" rel="noopener"><strong>CircLinkList.h</strong></a></td><td style="text-align:center"><a href="https://github.com/sundongxu/data-structure/blob/master/CircLinkList.cc" target="_blank" rel="noopener"><strong>CircLinkList.cc</strong></a></td></tr></tbody></table><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><hr><p>[1]<a href="https://www.amazon.cn/dp/B001TREOXM" target="_blank" rel="noopener">数据结构与算法(C++版) - 唐宁九主编</a><br>[2]<a href="https://www.amazon.cn/dp/B0011F7UHO" target="_blank" rel="noopener">数据结构(用面向对象方法与C++语言描述) - 殷人昆主编</a></p>]]></content>
<categories>
<category> Algorithm </category>
</categories>
<tags>
<tag> Data Structure </tag>
<tag> Linear List </tag>
<tag> Linked List </tag>
<tag> C++ </tag>
</tags>
</entry>
<entry>
<title>LeetCode 刷题纪要</title>
<link href="/2018/03/28/Algorithm/LeetCode/memo/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><p>算法修炼,结构通透,逻辑分明。<br>滴水穿石,非一日之功。<br><a id="more"></a></p><h2 id="1st-Round"><a href="#1st-Round" class="headerlink" title="1st Round"></a>1st Round</h2><h3 id="Day1"><a href="#Day1" class="headerlink" title="Day1"></a>Day1</h3><hr><p><strong>2018-03-23, Sunny.</strong><br><strong>7 Problems Finished.</strong></p><table><thead><tr><th style="text-align:center">No</th><th style="text-align:center">Title</th><th style="text-align:center">Difficulty</th></tr></thead><tbody><tr><td style="text-align:center">1</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/1-TwoSum.cc" target="_blank" rel="noopener"><strong>Two Sum</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">7</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/7-ReverseInteger.cc" target="_blank" rel="noopener"><strong>Reverse Integer</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">9</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/9-PalindromeNumber.cc" target="_blank" rel="noopener"><strong>Palindrome Number</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">13</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/13-RomanToInteger.cc" target="_blank" rel="noopener"><strong>Roman to Integer</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">14</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/14-LongestCommonPrefix.cc" target="_blank" rel="noopener"><strong>Longest Common Prefix</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">39</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/39-CombinationSum.cc" target="_blank" rel="noopener"><strong>Combination Sum(DFS)</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">593</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/593-ValidSquare.cc" target="_blank" rel="noopener"><strong>Valid Square</strong></a></td><td style="text-align:center">Medium</td></tr></tbody></table><h3 id="Day2"><a href="#Day2" class="headerlink" title="Day2"></a>Day2</h3><hr><p><strong>2018-03-24, Wild.</strong><br><strong>10 Problems Finished.</strong></p><table><thead><tr><th style="text-align:center">No</th><th style="text-align:center">Title</th><th style="text-align:center">Difficulty</th></tr></thead><tbody><tr><td style="text-align:center">8</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/8-StringToIntegerAToI.cc" target="_blank" rel="noopener"><strong>String To Integer(atoi</strong>)</a></td><td style="text-align:center">Meidum</td></tr><tr><td style="text-align:center">21</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/21-MergeTwoSortedLists.cc" target="_blank" rel="noopener"><strong>Merge Two Sorted Lists</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">23</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Hard/23-MergeKSortedLists.cc" target="_blank" rel="noopener"><strong>Merge K Sorted Lists</strong></a></td><td style="text-align:center">hard</td></tr><tr><td style="text-align:center">26</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/26-RemoveDuplicatesFromSortedArray.cc" target="_blank" rel="noopener"><strong>Remove Duplicates From Sorted Array</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">27</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/27-RemoveElement.cc" target="_blank" rel="noopener"><strong>Remove Element</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">28</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/28-ImplementStrstr.cc" target="_blank" rel="noopener"><strong>Implement strStr</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">80</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/80-RemoveDuplicatesFromSortedArray2.cc" target="_blank" rel="noopener"><strong>Remove Duplicates From Sorted Array(2)</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">88</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/88-MergeSortedArray.cc" target="_blank" rel="noopener"><strong>Merge Sorted Array</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">168</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/168-ExcelSheetColumnTitle.cc" target="_blank" rel="noopener"><strong>Excel Sheet Column Title</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">171</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/171-ExcelSheetColumnNumber.cc" target="_blank" rel="noopener"><strong>Exvel Sheet Column Number</strong></a></td><td style="text-align:center">Easy</td></tr></tbody></table><p><strong>some tip</strong>: <a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/trick_sync_with_stdio.cc" target="_blank" rel="noopener"><strong>trick_sync_with_stdio.cc</strong></a></p><h3 id="Day3"><a href="#Day3" class="headerlink" title="Day3"></a>Day3</h3><hr><p><strong>2018-03-26, Hot.</strong><br><strong>15 Problems Finished.</strong></p><table><thead><tr><th style="text-align:center">No</th><th style="text-align:center">Title</th><th style="text-align:center">Difficulty</th></tr></thead><tbody><tr><td style="text-align:center">2</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/2-AddTwoNumbers.cc" target="_blank" rel="noopener"><strong>Add Two Numbers</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">34</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/34-SearchForARange.cc" target="_blank" rel="noopener"><strong>Search For A Range</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">35</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/35-SearchInsertPosition.cc" target="_blank" rel="noopener"><strong>Search Insert Position</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">38</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/38-CountAndSay.cc" target="_blank" rel="noopener"><strong>Count And Say</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">50</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/50-Pow.cc" target="_blank" rel="noopener"><strong>Pow</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">53</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/53-MaximumSubArray.cc" target="_blank" rel="noopener"><strong>Maximum SubArray</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">58</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/58-LengthOfLastWord.cc" target="_blank" rel="noopener"><strong>Length Of Last Word</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">64</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/64-MinimumPathSum.cc" target="_blank" rel="noopener"><strong>Minimum Path Sum</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">66</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/66-PlusOne.cc" target="_blank" rel="noopener"><strong>Plus One</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">67</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/67-AddBinary.cc" target="_blank" rel="noopener"><strong>Add Binary</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">69</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/69-Sqrt.cc" target="_blank" rel="noopener"><strong>Sqrt</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">70</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/70-ClimbingStairs.cc" target="_blank" rel="noopener"><strong>Climbing Stairs</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">112</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/112-PathSum.cc" target="_blank" rel="noopener"><strong>Path Sum</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">113</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/113-PathSum2.cc" target="_blank" rel="noopener"><strong>Path Sum(2)</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">120</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/120-Triangle.cc" target="_blank" rel="noopener"><strong>Triangle</strong></a></td><td style="text-align:center">Medium</td></tr></tbody></table><h3 id="Day4"><a href="#Day4" class="headerlink" title="Day4"></a>Day4</h3><hr><p><strong>2018-03-27, Hot.</strong><br><strong>8 Problems Finished.</strong></p><table><thead><tr><th style="text-align:center">No</th><th style="text-align:center">Title</th><th style="text-align:center">Difficulty</th></tr></thead><tbody><tr><td style="text-align:center">83</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/83-RemoveDuplicatesFromSortedList.cc" target="_blank" rel="noopener"><strong>Remove Duplicates From Sorted List</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">94</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/94-BinaryTreeInorderTraversal.cc" target="_blank" rel="noopener"><strong>Binary Tree Inorder Traversal</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">144</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/144-BinaryTreePreorderTraversal.cc" target="_blank" rel="noopener"><strong>Binary Tree Preorder Traversal</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">145</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Hard/145-BinaryTreePostorderTraversal.cc" target="_blank" rel="noopener"><strong>Binary Tree Postorder Traversal</strong></a></td><td style="text-align:center">Hard</td></tr><tr><td style="text-align:center">102</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/102-BinaryTreeLevelOrderTraversal.cc" target="_blank" rel="noopener"><strong>Binary Tree Level Order Traversal</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">107</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/107-BinaryTreeLevelOrderTraversal2.cc" target="_blank" rel="noopener"><strong>Binary Tree Level Order Traversal(2)</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">100</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/100-SameTree.cc" target="_blank" rel="noopener"><strong>Same Tree</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">101</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/101-SymmetricTree.cc" target="_blank" rel="noopener"><strong>SymmetricTree</strong></a></td><td style="text-align:center">Easy</td></tr></tbody></table><h3 id="Day5"><a href="#Day5" class="headerlink" title="Day5"></a>Day5</h3><hr><p><strong>2018-03-28, Too Hot.</strong><br><strong>8 Problems Finished.</strong></p><table><thead><tr><th style="text-align:center">No</th><th style="text-align:center">Title</th><th style="text-align:center">Difficulty</th></tr></thead><tbody><tr><td style="text-align:center">11</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/11-ContainerWithMostWater.cc" target="_blank" rel="noopener"><strong>Container With Most Water</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">62</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/62-UniquePath.cc" target="_blank" rel="noopener"><strong>Unique Path</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">104</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/104-MaximumDepthOfBinaryTree.cc" target="_blank" rel="noopener"><strong>Maximum Depth Of Binary Tree</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">110</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/110-BalancedBinaryTree.cc" target="_blank" rel="noopener"><strong>Balanced Binary Tree</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">111</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/111-MinimumDepthOfBinaryTree.cc" target="_blank" rel="noopener"><strong>Minimum Depth Of Binary Tree</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">114</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/114-FlattenBinaryTreeToLinkedList.cc" target="_blank" rel="noopener"><strong>Flatten Binary Tree To LinkedList</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">118</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/118-PascalTriangle.cc" target="_blank" rel="noopener"><strong>Pascal Triangle</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">617</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/617-MergeTwoBinaryTrees.cc" target="_blank" rel="noopener"><strong>Merge Two Binary Trees</strong></a></td><td style="text-align:center">Easy</td></tr></tbody></table><h3 id="Day6"><a href="#Day6" class="headerlink" title="Day6"></a>Day6</h3><hr><p><strong>2018-04-01, Wild.</strong><br><strong>11 Problems Finished.</strong></p><table><thead><tr><th style="text-align:center">No</th><th style="text-align:center">Title</th><th style="text-align:center">Difficulty</th></tr></thead><tbody><tr><td style="text-align:center">15</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/15-3Sum.cc" target="_blank" rel="noopener"><strong>3 Sum</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">16</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/16-3SumClosest.cc" target="_blank" rel="noopener"><strong>3 Sum Closest</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">18</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/18-4Sum.cc" target="_blank" rel="noopener"><strong>4 Sum</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">24</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/24-SwapNodesInPairs.cc" target="_blank" rel="noopener"><strong>Swap Nodes In Pairs</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">25</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Hard/25-ReverseNodesInKGroup.cc" target="_blank" rel="noopener"><strong>Reverse Nodes In K-Group</strong></a></td><td style="text-align:center">Hard</td></tr><tr><td style="text-align:center">61</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/61-RotateList.cc" target="_blank" rel="noopener"><strong>Rotate List</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">86</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/86-PartitionList.cc" target="_blank" rel="noopener"><strong>Partition List</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">92</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/92-ReverseLinkedList2.cc" target="_blank" rel="noopener"><strong>Reverse Linked List(2)</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">147</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/147-InsertionSortList.cc" target="_blank" rel="noopener"><strong>Insertion Sort List</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">148</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/148-SortList.cc" target="_blank" rel="noopener"><strong>Sort List</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">206</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/206-ReverseLinkedList.cc" target="_blank" rel="noopener"><strong>Reverse Linked List</strong></a></td><td style="text-align:center">Easy</td></tr></tbody></table><h3 id="Day7"><a href="#Day7" class="headerlink" title="Day7"></a>Day7</h3><hr><p><strong>2018-04-02, Wild.</strong><br><strong>12 Problems Finished.</strong></p><table><thead><tr><th style="text-align:center">No</th><th style="text-align:center">Title</th><th style="text-align:center">Difficulty</th></tr></thead><tbody><tr><td style="text-align:center">121</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/121-BestTimeToBuyAndSellStock.cc" target="_blank" rel="noopener"><strong>Best Time To Buy And Sell Stock</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">122</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/122-BestTimeToBuyAndSellStock2.cc" target="_blank" rel="noopener"><strong>Best Time To Buy And Sell Stock(2)</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">123</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Hard/123-BestTimeToBuyAndSellStock3.cc" target="_blank" rel="noopener"><strong>Best Time To Buy And Sell Stock(3)</strong></a></td><td style="text-align:center">Hard</td></tr><tr><td style="text-align:center">125</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/125-ValidPalindrome.cc" target="_blank" rel="noopener"><strong>Valid Palindrome</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">136</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/136-SingleNumber.cc" target="_blank" rel="noopener"><strong>Single Number</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">137</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/137-SingleNumber2.cc" target="_blank" rel="noopener"><strong>SingleNumber2</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">141</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/141-LinkedListCycle.cc" target="_blank" rel="noopener"><strong>Linked List Cycle</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">142</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/142-LinkedListCycle2.cc" target="_blank" rel="noopener"><strong>Linked List Cycle(2)</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">155</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/155-MinStack.cc" target="_blank" rel="noopener"><strong>Min Stack</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">159</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/159-MajorityElement.cc" target="_blank" rel="noopener"><strong>Majority Element</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">160</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/160-IntersectionOfTwoLinkedList.cc" target="_blank" rel="noopener"><strong>Intersection Of Two Linked List</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">172</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/172-FactorialTrailingZeroes.cc" target="_blank" rel="noopener"><strong>Factorial Trailing Zeroes</strong></a></td><td style="text-align:center">Easy</td></tr></tbody></table><h3 id="Day8"><a href="#Day8" class="headerlink" title="Day8"></a>Day8</h3><hr><p><strong>2018-04-03, Cool.</strong><br><strong>6 Problems Finished.</strong></p><table><thead><tr><th style="text-align:center">No</th><th style="text-align:center">Title</th><th style="text-align:center">Difficulty</th></tr></thead><tbody><tr><td style="text-align:center">189</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/189-RotateArray.cc" target="_blank" rel="noopener"><strong>Rotate Array</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">190</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/190-ReverseBits.cc" target="_blank" rel="noopener"><strong>Reverse Bits</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">191</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/191-NumberOfOneBits.cc" target="_blank" rel="noopener"><strong>Number Of One Bits</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">198</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/198-HouseRobber.cc" target="_blank" rel="noopener"><strong>House Robber</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">213</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/213-HouseRobber2.cc" target="_blank" rel="noopener"><strong>House Robber(2)</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">231</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/231-PowerOfTwo.cc" target="_blank" rel="noopener"><strong>Power Of Two</strong></a></td><td style="text-align:center">Easy</td></tr></tbody></table><h3 id="Day9"><a href="#Day9" class="headerlink" title="Day9"></a>Day9</h3><hr><p><strong>2018-04-09, Sunny.</strong><br><strong>14 Problems Finished.</strong></p><table><thead><tr><th style="text-align:center">No</th><th style="text-align:center">Title</th><th style="text-align:center">Difficulty</th></tr></thead><tbody><tr><td style="text-align:center">3</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/3-LongestSubstringWithoutRepeatingCharacters.cc" target="_blank" rel="noopener"><strong>Longest Substring Without Repeating Characters</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">5</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/5-LongestPalindromicSubstring.cc" target="_blank" rel="noopener"><strong>Longest Palindromic Substring</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">6</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/6-ZigZagConversion.cc" target="_blank" rel="noopener"><strong>Zigzag Conversion</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">17</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/17-LetterCombinationOfAPhoneNumber.cc" target="_blank" rel="noopener"><strong>Letter Combination Of A Phone Number</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">19</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/19-RemoveNthNodeFromEndOfList.cc" target="_blank" rel="noopener"><strong>Remove Nth Node From End of List</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">22</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/22-GenerateParentheses.cc" target="_blank" rel="noopener"><strong>Generate Parentheses</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">29</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/29-DivideTwoIntegers.cc" target="_blank" rel="noopener"><strong>Divide Two Integers</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">31</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/31-NextPermutation.cc" target="_blank" rel="noopener"><strong>Next Permutation</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">103</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/103-BinaryTreeZigzagLevelOrderTraversal.cc" target="_blank" rel="noopener"><strong>Binary Tree Zigzag Level Order Traversal</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">116</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/116-PoplulatingNextRightPointersInEachNode.cc" target="_blank" rel="noopener"><strong>Populating Next Right Pointers In Each Node</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">117</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/117-PopulatingNextRightPointersInEachNode2.cc" target="_blank" rel="noopener"><strong>Populating Next Right Pointers In Each Node(2)</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">192</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/192-WordFrequency.cc" target="_blank" rel="noopener"><strong>Word Frequecy</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">215</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/215-KthLargestElementInAnArray.cc" target="_blank" rel="noopener"><strong>Kth Largest Element In An Array</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">347</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/347-TopKFrequentElements.cc" target="_blank" rel="noopener"><strong>Top K Frequent Elements</strong></a></td><td style="text-align:center">Medium</td></tr></tbody></table><h3 id="Day10"><a href="#Day10" class="headerlink" title="Day10"></a>Day10</h3><hr><p><strong>2018-04-10, Hot.</strong><br><strong>6 Problems Finished.</strong></p><table><thead><tr><th style="text-align:center">No</th><th style="text-align:center">Title</th><th style="text-align:center">Difficulty</th></tr></thead><tbody><tr><td style="text-align:center">10</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Hard/10-RegularExpressionMatching.cc" target="_blank" rel="noopener"><strong>Regular Expression Matching</strong></a></td><td style="text-align:center">Hard</td></tr><tr><td style="text-align:center">44</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Hard/44-WildcardMatching.cc" target="_blank" rel="noopener"><strong>Wildcard Matching</strong></a></td><td style="text-align:center">Hard</td></tr><tr><td style="text-align:center">371</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/371-SumOfTwoIntegers.cc" target="_blank" rel="noopener"><strong>Sum Of Two Integers</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">434</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/434-NumberOfSegmentsInAString.cc" target="_blank" rel="noopener"><strong>Number Of Segments In A String</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">459</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/459-RepeatedSubstringPattern.cc" target="_blank" rel="noopener"><strong>Repeated Substring Pattern</strong></a></td><td style="text-align:center">Easy</td></tr><tr><td style="text-align:center">686</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Easy/686-RepeatedStringMatch.cc" target="_blank" rel="noopener"><strong>Repeated String Match</strong></a></td><td style="text-align:center">Easy</td></tr></tbody></table><h3 id="Day11"><a href="#Day11" class="headerlink" title="Day11"></a>Day11</h3><hr><p><strong>2018-04-14, Hot.</strong><br><strong>10 Problems Finished.</strong></p><table><thead><tr><th style="text-align:center">No</th><th style="text-align:center">Title</th><th style="text-align:center">Difficulty</th></tr></thead><tbody><tr><td style="text-align:center">33</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/33-SearchInRotatedSortedArray.cc" target="_blank" rel="noopener"><strong>Search In Rotated Sorted Array</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">36</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/36-ValidSudoku.cc" target="_blank" rel="noopener"><strong>Valid Sudoku</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">43</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/43-MultiplyStrings.cc" target="_blank" rel="noopener"><strong>Multiply Strings</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">46</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/46-Permutations.cc" target="_blank" rel="noopener"><strong>Permutations</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">47</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/47-Permutations2.cc" target="_blank" rel="noopener"><strong>Permutations(2)</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">48</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/48-RotateImage.cc" target="_blank" rel="noopener"><strong>Rotate Image</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">60</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/60-PermutationSequence.cc" target="_blank" rel="noopener"><strong>Permutation Sequence</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">77</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/77-Combinations.cc" target="_blank" rel="noopener"><strong>Combinations</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">81</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/81-SearchInRotatedSortedArray2.cc" target="_blank" rel="noopener"><strong>Search In Rotated Sorted Array(2)</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">153</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/153-FindMinimumInRotatedSortedArray.cc" target="_blank" rel="noopener"><strong>Find Minimum In Rotated Sorted Array</strong></a></td><td style="text-align:center">Medium</td></tr></tbody></table><h3 id="Day12"><a href="#Day12" class="headerlink" title="Day12"></a>Day12</h3><hr><p><strong>2018-04-15, Hot.</strong><br><strong>12 Problems Finished.</strong></p><table><thead><tr><th style="text-align:center">No</th><th style="text-align:center">Title</th><th style="text-align:center">Difficulty</th></tr></thead><tbody><tr><td style="text-align:center">49</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/49-GroupAnagrams.cc" target="_blank" rel="noopener"><strong>Group Anagrams</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">54</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/54-SpiralMatrix.cc" target="_blank" rel="noopener"><strong>Spiral Matrix</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">55</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/55-JumpGame.cc" target="_blank" rel="noopener"><strong>Jump Game</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">56</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/56-MergeIntervals.cc" target="_blank" rel="noopener"><strong>Merge Intervals</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">57</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Hard/57-InsertInterval.cc" target="_blank" rel="noopener"><strong>Insert Interval</strong></a></td><td style="text-align:center">Hard</td></tr><tr><td style="text-align:center">59</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/59-SpiralMatrix2.cc" target="_blank" rel="noopener"><strong>Spiral Matrix(2)</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">63</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/63-UniquePath2.cc" target="_blank" rel="noopener"><strong>Unique Path(2)</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">71</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/71-SimplifyPath.cc" target="_blank" rel="noopener"><strong>Simplify Path</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">73</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/73-SetMatrixZeroes.cc" target="_blank" rel="noopener"><strong>Set Matrix Zeroes</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">289</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/289-GameOfLife.cc" target="_blank" rel="noopener"><strong>Game Of Life</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">365</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/365-WaterAndJugProblem.cc" target="_blank" rel="noopener"><strong>Water And Jug Problem</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">786</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Hard/786-KthSmallestPrimeFraction.cc" target="_blank" rel="noopener"><strong>Kth Smallest Prime Fraction</strong></a></td><td style="text-align:center">Hard</td></tr><tr><td style="text-align:center">59</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/59-SpiralMatrix2.cc" target="_blank" rel="noopener"><strong>Spiral Matrix(2)</strong></a></td><td style="text-align:center">Medium</td></tr></tbody></table><h3 id="Day13"><a href="#Day13" class="headerlink" title="Day13"></a>Day13</h3><hr><p><strong>2018-04-21, Cool.</strong><br><strong>10 Problems Finished.</strong></p><table><thead><tr><th style="text-align:center">No</th><th style="text-align:center">Title</th><th style="text-align:center">Difficulty</th></tr></thead><tbody><tr><td style="text-align:center">74</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/74-SearchA2DMatrix.cc" target="_blank" rel="noopener"><strong>Search A 2D Matrix</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">75</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/75-SortColors.cc" target="_blank" rel="noopener"><strong>Sort Colors</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">78</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/78-Subsets.cc" target="_blank" rel="noopener"><strong>Subsets</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">79</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/79-WordSearch.cc" target="_blank" rel="noopener"><strong>Word Search</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">82</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/82-RemoveDuplicatesFromSortedList2.cc" target="_blank" rel="noopener"><strong>Remove Duplicates From Sorted List(2)</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">89</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/89-GrayCode.cc" target="_blank" rel="noopener"><strong>GrayCode</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">90</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/90-Subsets2.cc" target="_blank" rel="noopener"><strong>Subsets(2)</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">146</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Hard/146-LRUCache.cc" target="_blank" rel="noopener"><strong>LRUCache</strong></a></td><td style="text-align:center">Hard</td></tr><tr><td style="text-align:center">373</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/373-FindKPairsWithSmallestSums.cc" target="_blank" rel="noopener"><strong>Find K Pairs With Smallest Sums</strong></a></td><td style="text-align:center">Medium</td></tr><tr><td style="text-align:center">378</td><td style="text-align:center"><a href="https://github.com/sundongxu/LeetCode/blob/master/Difficulty/Medium/378-KthSmallestElementInASortedMatrix.cc" target="_blank" rel="noopener"><strong>Kth Smallest Element In A Sorted Matrix</strong></a></td><td style="text-align:center">Medium</td></tr></tbody></table>]]></content>
<categories>
<category> Algorithm </category>
</categories>
<tags>
<tag> Algorithm </tag>
<tag> C++ </tag>
<tag> LeetCode </tag>
</tags>
</entry>
<entry>
<title>Shadowsocks 搭梯全教程</title>
<link href="/2018/01/31/OS/Installation/Ubuntu/ladder/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><hr><p>正为各种 <a href="https://zh.wikipedia.org/wiki/Vpn" target="_blank" rel="noopener"><strong>VPN</strong></a> 被封或是网上那些不太靠谱的翻墙教程所困扰?</p><p>刚好,这里有一份 <a href="https://zh.wikipedia.org/wiki/Shadowsocks" target="_blank" rel="noopener"><strong>Shadowsocks</strong></a> 指南,包含 <strong>Linux</strong> 和 <strong>MacOS</strong> 系统下的安装与配置,以及在 <strong>浏览器</strong> 和 <strong>终端</strong> 下翻墙的姿势介绍。</p><p>没错,这很可能就是你见过的最完整、最易懂的VPN搭建教程。<br><a id="more"></a></p><h2 id="前面的话"><a href="#前面的话" class="headerlink" title="前面的话"></a>前面的话</h2><hr><p><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/gfw.png" alt="image"></p><p>在当前这个知识爆炸的时代,作为一名学术研究者或技术钻研者,搜索引擎无疑是最好的学习工具,而 <a href="https://www.google.com" target="_blank" rel="noopener"><strong>Google</strong></a>正是个中翘楚。但早在互联网广泛流行于中国之前,出于一些不可抗原因,<a href="https://zh.wikipedia.org/wiki/%E9%98%B2%E7%81%AB%E9%95%BF%E5%9F%8E" target="_blank" rel="noopener"><strong>GFW</strong></a> 横空出世,国外网站对于内地网民从此遥不可及。好在技术无极限,更无国界,防火墙挡住了我们通往外界的视野,搭个梯子翻过去便是。</p><p>于是,科学上网正成为当今知识分子的必备技能。而市场上已经有不少搭好的“梯子”,各类付费 <strong>VPN</strong> 琳琅满目,购买套餐后按照提示配置即可,方便快捷,但问题是不够稳妥安全。换句话说,购买这些 <strong>VPN</strong> 是比较危险的,很容易就被…你懂的。例如,笔者亲身经历过头天刚下决心买了某老牌付费 <strong>VPN</strong> 的两年会员,第二天就被封的…</p><p>所以最好的方法是购买小众 <a href="https://zh.wikipedia.org/wiki/%E8%99%9A%E6%8B%9F%E4%B8%93%E7%94%A8%E6%9C%8D%E5%8A%A1%E5%99%A8" target="_blank" rel="noopener"><strong>VPS</strong></a> 搭建自己的海外服务器,防止被误伤。作为第一次捣鼓这些的新手,我是十分不建议上来就花个几十美金购买 <strong>VPS</strong> 的,最好的方案是找一个免费的服务器来练练手,那么,亚马逊就站出来了,他家的 <a href="https://amazonaws-china.com/cn/" target="_blank" rel="noopener"><strong>Amazon Web Services</strong></a>,在一定条件下能免费使用一年。</p><p>当然,你必须有一张信用卡,没有的话,上某宝买张虚拟信用卡,操作也很easy的啦。</p><p>注:</p><blockquote><p>Virtual Private Server, 简称 VPS,虚拟专用服务器<br>Virtual Private Network, 简称 VPN,虚拟专用网络</p></blockquote><h2 id="Shadowsocks"><a href="#Shadowsocks" class="headerlink" title="Shadowsocks"></a>Shadowsocks</h2><hr><h3 id="前世今生"><a href="#前世今生" class="headerlink" title="前世今生"></a>前世今生</h3><p><a href="https://github.com/shadowsocks/shadowsocks/tree/master" target="_blank" rel="noopener"><strong>Shadowsocks</strong></a> 是目前最好的科学上网方式,它的流量经过加密,所以没有流量特征不会被GFW阻断;关键是,它的实现原理也通俗易懂。</p><h4 id="long-long-ago"><a href="#long-long-ago" class="headerlink" title="long long ago"></a>long long ago</h4><p>在很久很久以前,我们访问各种网站都是简单而直接的,用户的请求通过互联网发送到服务提供方,服务提供方直接将信息反馈给用户。<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/gfw-before.png" alt="image"></p><h4 id="when-devil-comes"><a href="#when-devil-comes" class="headerlink" title="when devil comes"></a>when devil comes</h4><p>然后有一天,<strong>GFW</strong> 就出现了,他像一个收过路费的强盗一样夹在了在用户和服务之间,每当用户需要获取信息,都经过了 <strong>GFW</strong>,<strong>GFW</strong>将它不喜欢的内容统统过滤掉,于是客户当触发 <strong>GFW</strong> 的过滤规则的时候,就会收到 <strong>Connection Reset</strong> 这样的响应内容,而无法接收到正常的内容。<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/gfw-now.png" alt="image"></p><h4 id="ssh-tunnel"><a href="#ssh-tunnel" class="headerlink" title="ssh tunnel"></a>ssh tunnel</h4><p>聪明的人们想到了利用境外服务器代理的方法来绕过 <strong>GFW</strong> 的过滤,其中包含了各种 <strong>HTTP</strong> 代理服务、<strong>Socks</strong> 服务、<strong>VPN</strong> 服务… 其中以 <strong>ssh tunnel</strong> 的方法比较有代表性:</p><blockquote><p>1) 首先用户和境外服务器基于 ssh 建立起一条加密的通道<br>2-3) 用户通过建立起的隧道进行代理,通过 ssh server 向真实的服务发起请求<br>4-5) 服务通过 ssh server,再通过创建好的隧道返回给用户</p></blockquote><p><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/gfw-ssh-tunnel.png" alt="image"></p><p>由于 <strong>ssh</strong> 本身就是基于 <strong>RSA</strong> 加密技术,所以 <strong>GFW</strong> 无法从数据传输的过程中的加密数据内容进行关键词分析,避免了被重置链接的问题,但由于创建隧道和数据传输的过程中,<strong>ssh</strong> 本身的特征是明显的,所以 <strong>GFW</strong> 一度通过分析连接的特征进行干扰,导致 <strong>ssh</strong> 存在被定向进行干扰的问题。</p><h4 id="shadowsocks"><a href="#shadowsocks" class="headerlink" title="shadowsocks"></a>shadowsocks</h4><p>@<a href="https://github.com/clowwindy" target="_blank" rel="noopener"><strong>clowwindy</strong></a> 大神分享并开源了他的解决方案</p><p>简单理解的话,<strong>Shadowsocks</strong> 是将原来 <strong>ssh</strong> 创建的 <strong>socks5</strong> 协议拆开成 <strong>server</strong> 端和 <strong>client</strong> 端,所以下面这个原理图基本上和利用 <strong>ssh tunnel</strong> 大致类似。</p><blockquote><p>1、6) 客户端发出的请求基于 Socks5 协议跟 ss-local 端进行通讯,由于这个 ss-local 一般是本机或路由器或局域网的其他机器,不经过 GFW,所以解决了上面被 GFW 通过特征分析进行干扰的问题<br>2、5) ss-local 和 ss-server 两端通过多种可选的加密方法进行通讯,经过 GFW 的时候是常规的TCP包,没有明显的特征码而且 GFW 也无法对通讯数据进行解密<br>3、4) ss-server 将收到的加密数据进行解密,还原原来的请求,再发送到用户需要访问的服务,获取响应原路返回</p></blockquote><p><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/gfw-shadowsocks.png" alt="image"></p><h3 id="基本原理"><a href="#基本原理" class="headerlink" title="基本原理"></a>基本原理</h3><p>从源码角度分析,<strong>Shadowsocks</strong> 的工作原理并不复杂。<strong>Shadowsocks</strong> 包括 <strong>local.py</strong> 和 <strong>server.py</strong> 两个程序:<strong>local</strong> 运行在用户自己的机器上,<strong>server</strong> 运行在墙外的服务器上。正常工作模式下,<strong>local</strong> 通常会监听本地 <strong>1080</strong> 端口,提供 <strong>socks5</strong> 协议接口。其工作流程简述如下:</p><blockquote><ol><li>在用户本机进程和 local 的 1080 端口建立 TCP 连接之后,首先会发送一个 hello 包,或称 handshake 握手包;</li><li>local 程序接收到这个包之后,进行简单的包数据检查之后就返回一个确认包;</li><li>本机进程收到确认的包之后,会再发送一个请求包,包的主要内容就是目标服务的地址和端口号;</li><li>local 程序接收到请求包之后,会和 server 程序建立一个 TCP 连接,连接建立之后会把上面的目标服务的地址和端口号传给 server。这个连接是穿墙的关键,连接里面传输的数据都是经过加密的,最常用的就是 aes-256-cfb;</li><li>local 程序会对请求包返回一个确认包,告知本机进程其与 server 程序的连接已建立,等待数据请求的传输;</li><li>然后本机进程就开始向 local 传输实际的数据,local 接收到之后加密继续传给 server;</li><li>server 接收到之后把数据解密出来,然后和目标服务建立 TCP 连接,并把解密后的数据请求发送出去;</li><li>server 接收到请求的数据后最终返回给本机进程就是上述的反过程。</li></ol></blockquote><h3 id="实例图解"><a href="#实例图解" class="headerlink" title="实例图解"></a>实例图解</h3><p><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/shadowsocks-basics-cn.png" alt="image"></p><p>ss要求本机运行local.py,海外服务器运行server.py。local.py默认监听localhost的1080端口,该端口代理浏览器的请求。browser要访问google时,会和localhost:1080进行一次基于sock5协议的通信,如上图的红色虚线框,sock5协议可以去了解下,维基百科有不错的介绍。</p><p>代理的流程如下:</p><blockquote><ol><li>localhost:1080经过sock5协议后,就知道要访问google了</li><li>local程序会把流量加密,然后把普通的TCP流量发往海外服务器;</li><li>海外服务器收到请求后,解密得到要访问google</li><li>海外服务器请求google后把得到的数据加密返回给local</li><li>local解密返回给browser。</li></ol></blockquote><p><strong>ss</strong> 的解密和加密都基于用户设置的密码,所以 <strong>local</strong> 和 <strong>server</strong> 之间可以做到加密和解密的一致。</p><p>因此,<strong>Shadowsocks</strong> 的优点在于它通过流量混淆隐秘解决了 <strong>GFW</strong> 通过分析流量特征从而干扰的问题,这是它优于 <strong>SSH</strong> 和 <strong>VPN</strong> 翻墙的地方(但 <strong>VPN</strong> 更注重加密安全性)。缺点也依然明显,需要一点点技术和资源(墙外 <strong>VPS服务器</strong>)来搭建 <strong>Shadowsocks服务</strong>,好在已经有人搭建相应的服务出售翻墙服务了。当然你也完全可以搭建一个专属于自己的 <strong>Shadowsocks</strong> 服务,后文介绍了完整的安装与配置教程以供参考。</p><h2 id="Shadowsocks-服务器"><a href="#Shadowsocks-服务器" class="headerlink" title="Shadowsocks 服务器"></a>Shadowsocks 服务器</h2><hr><p>从前文了解到,<strong>Shadowsocks</strong> 服务由 <strong>local(客户端)</strong> 和 <strong>server(墙外服务器)</strong> 组成,先来看看如何搭建个人墙外 <strong>VPS</strong>。</p><p>作为初学者,<strong>Amazon EC2</strong> 提供的免费海外服务器已经完全够用,本节介绍利用其搭建 <strong>Shadowsocks</strong> 服务器的步骤。</p><h3 id="注册Amazon"><a href="#注册Amazon" class="headerlink" title="注册Amazon"></a>注册Amazon</h3><p>点击<a href="http://aws.amazon.com/cn/" target="_blank" rel="noopener"><strong>此处</strong></a> 右上角注册,一路下来资料照填,之前有亚马逊账号的话就直接登录。</p><p>需要注意的一点是:<strong>注册过程需要绑定信用卡</strong>。</p><p>说好是免费的,怎么还要绑定信用卡扣费呢?所谓免费,就是在你没有用超的情况下(作为 <strong>Shadowsocks</strong> 服务器,唯一能超限使用的就是网络流量,每月15G流量)。绑定信用卡应该会扣两笔钱,都是1美元,一笔是预授权,一笔应该是押金,1年后会返还,如果超限使用,顺便用着1美金抵扣。实在没有信用卡的学生党,到某宝上买一个一美元的虚拟信用卡,按要求输入店家发来的卡号、有效期,持卡人写“匿名”即可。</p><p>注册完会需要输入你的手机号,<strong>AWS</strong> 会拨通你的手机号,让你输入网页提示的一串数字即完成注册。</p><h3 id="创建EC2实例"><a href="#创建EC2实例" class="headerlink" title="创建EC2实例"></a>创建EC2实例</h3><p>用刚才注册好的账号登录并进入 <strong>AWS</strong> 控制台,点击 <strong>EC2 (云中的虚拟服务器)</strong>,如果需要改变服务器节点(地区)的话,右上角可以选择:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/ss-server-ec2.png" alt="image"></p><p>进入 <strong>EC2</strong> 管理控制台后,点击 <strong>启动实例</strong>:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/ss-server-ec2-boot.png" alt="image"></p><p>上图中可以看到已经有一个正在运行的实例,那是我之前创建的不用在意,新用户的话,已运行实例数量应为0。</p><p>之后进入 <strong>Amazon 系统映像(AMI)</strong> 选择,我选的是 <strong>Ubuntu Server 16.04 LTS(HVM),SSD Volume Type</strong>:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/ss-server-ami-ubuntu.png" alt="image"></p><p>然后选择 <strong>实例类型</strong>,默认的 <strong>符合条件的免费套餐</strong> 即可,点击 <strong>下一步:配置实例详细信息</strong>:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/ss-server-instance-type.png" alt="image"></p><p><strong>步骤3:配置实例</strong> 和 <strong>步骤4:添加存储</strong> 都是默认选项,直接点下一步跳过,直至 <strong>步骤5:添加标签</strong>,新建标签,键为 <strong>name</strong>,值为 <strong>ss-vpn</strong>,其实都可以任意指定,之后 <strong>创建密钥对</strong> 时会用到,之后下一步:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/ss-server-tag.png" alt="image"></p><p><strong>步骤6:配置安全组</strong>,添加规则如图所示,其中端口 <strong>8388</strong> 是与 <strong>Shadowsocks</strong> 相关的,也可以自由指定:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/ss-server-security-group.png" alt="image"></p><p>最后确定开始审核。这时候会提示生成密钥对,这个很重要,一定要保存好,没有这个密钥对是无法远程登录(如SSH)管理你的服务器的,下载密钥对 <strong>.pem</strong> 文件后才能启动实例:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/ss-server-key-download.png" alt="image"></p><p>查看启动日志,发现 <strong>EC2</strong> 实例成功启动:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/ss-server-boot-successful.png" alt="image"></p><h3 id="连接到服务器"><a href="#连接到服务器" class="headerlink" title="连接到服务器"></a>连接到服务器</h3><p>实例初始化完成后,就可以进行远程连接了,右键你的实例,点击连接。会弹出连接提示,如果你使用的是 <strong>Windows</strong>,可以查看使用 <strong>PuTTY</strong> 从 <strong>Windows</strong> 连接到 <strong>Linux</strong> 实例,如果你是 <strong>Linux</strong> 或者 <strong>Mac OS</strong>,可以直接通过 <strong>SSH</strong> 连接到你的服务器,具体可以查看亚马逊给出的文档 <strong>使用 SSH 连接到 Linux 实例</strong>。</p><p>记住下图中所示位置这个 <strong>公有IP</strong>,它是你的 <strong>Shadowsocks</strong> 的服务器IP:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/ss-server-public-ip-dns.png" alt="image"></p><p>按弹窗提示 <strong>SSH</strong> 连接到远程实例:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/ss-server-connect-ssh.png" alt="image"></p><h3 id="配置Shadowsocks"><a href="#配置Shadowsocks" class="headerlink" title="配置Shadowsocks"></a>配置Shadowsocks</h3><p><strong>SSH</strong> 连接到服务器之后,执行如下命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">$ sudo -s # 获取超级管理员权限</span><br><span class="line"># apt-get update # 更新源</span><br><span class="line"># apt-get install python-pip # 安装pip工具,用于安装shadowsocks</span><br><span class="line"># pip install shadowsocks # 安装shadowsocks</span><br><span class="line"># vim /etc/shadowsocks_conf.json # 编辑shadowsocks配置,内容见后文</span><br><span class="line"># ssserver -c /etc/shadowsocks_conf.json -d start # 启动shadowsocks服务器</span><br></pre></td></tr></table></figure></p><p><strong>shadowsocks_conf.json</strong> 配置内容如下:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> "server":"0.0.0.0",</span><br><span class="line"> "port_password": {</span><br><span class="line"> "[这里写你的端口号,通常为8388]": "[密码1]",</span><br><span class="line"> "[其他端口号]": "[密码2]" # 一个其实就够了...可能这里也是可以多端口登录吧</span><br><span class="line"> },</span><br><span class="line"> "local_address": "127.0.0.1",</span><br><span class="line"> "local_port":1080,</span><br><span class="line"> "timeout":600,</span><br><span class="line"> "method":"aes-256-cfb",</span><br><span class="line"> "auth": true</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>配置字段说明如下:</p><table><thead><tr><th style="text-align:left">字段</th><th style="text-align:left">含义</th></tr></thead><tbody><tr><td style="text-align:left">server</td><td style="text-align:left">服务端监听地址(IPv4或IPv6)</td></tr><tr><td style="text-align:left">server_port</td><td style="text-align:left">服务端端口</td></tr><tr><td style="text-align:left">local_address</td><td style="text-align:left">本地监听地址,缺省为443</td></tr><tr><td style="text-align:left">local_port</td><td style="text-align:left">本地监听端口,一般为1080</td></tr><tr><td style="text-align:left">password</td><td style="text-align:left">用以加密的密钥</td></tr><tr><td style="text-align:left">timeout</td><td style="text-align:left">超时时间(秒)</td></tr><tr><td style="text-align:left">method</td><td style="text-align:left">加密方法,默认为 aes-256-cfb</td></tr><tr><td style="text-align:left">fast_open</td><td style="text-align:left">是否启用 TCP-Fast-Open, true or false</td></tr><tr><td style="text-align:left">workers</td><td style="text-align:left">worker数量,仅在Unix和Linux下有效</td></tr></tbody></table><p> 编辑完上述配置文件后,一定记得执行最后一条命令以启动 <strong>Shadowsocks</strong> 服务。</p><p>至此,<strong>Shadowsocks</strong> 服务器,即位于墙外的 <strong>server</strong> 配置全部完成。</p><p>那么 <strong>local</strong>,也就是客户端呢?别着急,下一节就是啦~</p><h2 id="Shadowsocks-客户端"><a href="#Shadowsocks-客户端" class="headerlink" title="Shadowsocks 客户端"></a>Shadowsocks 客户端</h2><hr><h3 id="Linux"><a href="#Linux" class="headerlink" title="Linux"></a>Linux</h3><p>以 <strong>Ubuntu</strong> 为例,图形化客户端无疑最方便,推荐 <a href="https://github.com/shadowsocks/shadowsocks-qt5/releases" target="_blank" rel="noopener"><strong>Shadowsocks-qt5</strong></a>,安装也十分简单:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ sudo add-apt-repository ppa:hzwhuang/ss-qt5</span><br><span class="line">$ sudo apt-get update</span><br><span class="line">$ sudo apt-get install shadowsocks-qt5</span><br></pre></td></tr></table></figure></p><p>安装成功之后,按 <strong>win</strong> 键输入关键字后应该能找到该客户端,如图所示:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/shadowsocks-Qt5.png" alt="image"></p><p>启动客户端后,点击:connection -> add,新建到 <strong>Shadowsocks Server</strong> 的连接:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/shadowsocks-Qt5-add-connection.png" alt="image"></p><p>在弹窗中填入早前配置好的 <strong>Shadowsocks</strong> 服务器信息:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/shadowsocks-Qt5-server-config.png" alt="image"></p><p>保存设置后右键点击刚添加的条目,点击连接:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/shadowsocks-Qt5-connect.png" alt="image"></p><p>到这里,在 <strong>Ubuntu</strong> 下已经启动 <strong>local</strong> 程序,尽管它已经与墙外的 <strong>server</strong> 建立起连接并可以完成通信,但是如浏览器、终端等需要访问外网的本机进程而言,却仍然无法达到翻墙成功的效果,这是因为这些本机进程在试图直接访问外网服务,而这些访问请求都会被 <strong>GFW</strong> 根据其特殊的流量特征而被屏蔽过滤掉,导致访问失败。</p><p>解决方案无疑就是 <strong>Shadowsocks</strong> :浏览器、终端等本机进程需要以 <strong>Shadowsocks</strong> 为中间人将数据请求发送出去,即需要将发往被墙掉的外网服务的流量重定向到本地 <strong>Shadowsocks</strong> 客户端进程(<strong>local</strong>)。该进程默认监听本地端口 <strong>1080</strong>,因此需要将本机进程的流量发送至该端口上,这就涉及到特定进程的配置工作,主要是 <strong>浏览器</strong> 和 <strong>终端</strong>。</p><h4 id="浏览器"><a href="#浏览器" class="headerlink" title="浏览器"></a>浏览器</h4><h5 id="Chrome"><a href="#Chrome" class="headerlink" title="Chrome"></a>Chrome</h5><p>配置代理,将对被墙的服务的流量重定向至本地 <strong>Shadowsocks</strong> 客户端,即 <strong>local</strong> 。<strong>Chrome</strong> 插件 <strong>SwitchyOmega</strong> 即可实现流量选择性代理,需要从官方插件商店中下载安装,操作过程中你很快会发现一个问题:访问官方商店这一行为本身就需要翻墙…</p><p>好在 <strong>Chrome</strong> 还有另一款插件 <a href=""><strong>谷歌访问助手</strong></a> ,下载安装后浏览器即可访问 <strong>Google</strong> 相关服务,安装完成后在浏览器右上角插件栏可以看到:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/google-assistant-icon.png" alt="image"></p><p>看到这里你或许又有了问题:那为啥不干脆就使用 <strong>谷歌助手</strong> 插件翻墙就好了,还要 <strong>SwitchyOmega</strong> 干啥?</p><p>答案是:<strong>谷歌助手</strong> 只能让浏览器可以访问 <strong>Google</strong> 相关服务,如搜索、学术、地图、商店等等,但是其余被墙网站如 <strong>Youtobe</strong>、<strong>Facebook</strong> 和 <strong>Twitter</strong> 等仍然不可访问,而 <strong>SwitchyOmega</strong> 则帮助我们可以通过浏览器访问到世界上任何网站,再也不用受 <strong>GFW</strong> 的拦截。</p><p>下载安装好后,开始配置 <strong>SwitchyOmega</strong> 之前,首先需禁用 <strong>谷歌访问助手</strong>,原因是两个插件本质都是代理功能,会发生冲突,要想使 <strong>SwitchyOmega</strong> 生效,就必须先禁用冲突项 <strong>谷歌访问助手</strong>:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/google-assistant-disable.png" alt="image"></p><p>然后启用 <strong>SwitchyOmega</strong>:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/switchy-omega-enable.png" alt="image"></p><p>之后浏览器右上角插件栏出现一个黑色圆圈图标,点击并选择 <strong>选项</strong>:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/switchy-omega-icon.png" alt="image"></p><p>之后进入 <strong>SwitchyOmega</strong> 配置页面:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/switchy-omega-main.png" alt="image"></p><p>新建名为 <strong>ss</strong> 的情景模式,当然名称也可以自己起:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/switchy-omega-new-profile.png" alt="image"></p><p>保存配置后是这样:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/switchy-omega-ss.png" alt="image"></p><p>在 <strong>情景模式</strong> 下选择 <strong>auto switch</strong>:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/switchy-omega-autoswitch.png" alt="image"></p><p>点击 <strong>添加规则列表</strong>,将以下链接粘贴至网址框:</p><blockquote><p><a href="https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt" target="_blank" rel="noopener">https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt</a></p></blockquote><p>并一定完全按下图配置:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/switchy-omega-autoswitch-config.png" alt="image"></p><p>点击 <strong>立即更新情景模式</strong>,此时由于未保存修改会弹出如下窗口:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/switchy-omega-confirm.png" alt="image"></p><p>点击 <strong>应用选项</strong>,<strong>规则列表正文</strong> 框内显示出 <strong>gfwlist.txt</strong> 文件中包含的过滤域名列表:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/switchy-omega-gfwlist-head.png" alt="image"></p><p>搜索关键字 <strong>google</strong> 会找到对应过滤项:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/switchy-omega-gfwlist-google.png" alt="image"></p><p>往下翻还能找到很多平时“可望不可即“的站点,如 <strong>Facebook</strong>、<strong>Youtube</strong>、<strong>Vimeo</strong>、<strong>Steam</strong>…甚至 <strong>Tensorflow</strong>、<strong>GitHub</strong> 还有 <strong>Android</strong> … 这个恕我不是太能理解,学习都不让嘛…</p><p>保存修改后,点击浏览器右上角的 <strong>SwitchyOmega</strong> 插件图标,选中 <strong>auto switch</strong> 模式:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/switchy-omega-finish.png" alt="image"></p><p>测试相关被墙网站,成功访问,搞定!</p><h5 id="Firefox"><a href="#Firefox" class="headerlink" title="Firefox"></a>Firefox</h5><p>网上很多人推荐代理插件 <strong>FoxyProxy</strong>,配置过程参见<a href="http://www.aichengxu.com/linux/11086629.htm" target="_blank" rel="noopener"><strong>此篇</strong></a>,不过我反正搞了很久没弄好,只好放弃…</p><p>下面教你个简单好用的方法,为 <strong>FireFox</strong> 配置 <strong>Shadowsocks</strong> 的非全局代理:</p><p>先安装 <strong>Genpac</strong>,可以用 <strong>pip</strong> 安装:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo pip install genpac</span><br></pre></td></tr></table></figure></p><p>新建 <strong>Shadowsocks</strong> 文件夹存放 <strong>pac</strong> 文件:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ mkdir ~/shadowsocks</span><br><span class="line">$ cd shadowsocks</span><br></pre></td></tr></table></figure></p><p>生成 <strong>pac</strong> 文件:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ genpac --proxy="SOCKS5 127.0.0.1:1080" --gfwlist-proxy="SOCKS5 127.0.0.1:1080" -o autoproxy.pac --gfwlist-url="https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt"</span><br></pre></td></tr></table></figure></p><p>设置系统网络代理为 <strong>Automatic</strong>,即自动模式,配置 <strong>URL</strong> 则填入本地 <strong>pac</strong> 文件地址:</p><blockquote><p>file:///home/sundongxu/shadowsocks/autoproxy.pac</p></blockquote><p>具体见下图:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/network-proxy-automatic.png" alt="image"></p><p>最后点击在系统范围内应用:<strong>Apply System Wide</strong>,并按要求输入密码修改这一设置。</p><p>之后配置 <strong>Firefox</strong> 使用系统代理,进入浏览器 <strong>Preferences(偏好设置)</strong>:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/firefox-preference-proxy.png" alt="image"></p><p><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/firefox-use-system-proxy.png" alt="image"></p><p>至此,<strong>Ubuntu</strong> 下的 <strong>Firefox</strong> 浏览器也应该能够成功翻墙啦~</p><h4 id="终端"><a href="#终端" class="headerlink" title="终端"></a>终端</h4><p>相比浏览器而言,<strong>Ubuntu</strong> 下的终端翻墙则要容易得多,推荐 <strong>proxychains</strong>,安装配置极为简单:</p><p><strong>proxychains</strong> 安装:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo apt install proxychain</span><br></pre></td></tr></table></figure></p><p><strong>proxychains</strong> 配置:<br>编辑配置文件 <strong>/etc/proxychains.conf</strong>:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ sudo vim /etc/proxychains.conf</span><br><span class="line"># 注释最后一行:socks4 127.0.0.1 9050</span><br><span class="line"># 添加一行:socks5 127.0.0.1 1080</span><br></pre></td></tr></table></figure></p><p><strong>proxychains</strong> 测试:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ proxychains curl www.google.com</span><br></pre></td></tr></table></figure></p><p><strong>proxychains</strong> 使用:<br>用命令行启动软件,在前面加上 <strong>proxychains</strong>,如:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ proxychains firefox</span><br></pre></td></tr></table></figure></p><p>使用 <strong>Shadowsocks</strong> + <strong>proxychains</strong> 代理打开新的 <strong>firefox</strong> 实现浏览器翻墙。当然,最方便的就是直接输入 <strong>proxychain bash/zsh</strong> 新建一个新的 <strong>Shell</strong>,在新 <strong>Shell</strong> 中运行的所有命令都将使用代理成功翻墙。</p><h3 id="MacOS"><a href="#MacOS" class="headerlink" title="MacOS"></a>MacOS</h3><hr><p>同样推荐使用带 <strong>GUI</strong> 的 <strong>Shadowsocks</strong> 客户端,最好用的当属 <a href="https://github.com/shadowsocks/ShadowsocksX-NG/releases/" target="_blank" rel="noopener"><strong>ShadowsocksX-NG</strong></a>,下载 <strong>zip</strong> 文件后直接安装即可。</p><p>安装完成后会在 <strong>Launchpad</strong> 中找到其应用图标:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/shadowsocksX-NG.png" alt="image"></p><p>点击启动应用,<strong>Mac</strong> 桌面右上角会出现一个<strong>纸飞机</strong>图标,点击该图标,选择:服务器 -> 服务器设置:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/shadowsocksX-NG-add-server.png" alt="image"></p><p>之后在窗口中填入早先配置好的 <strong>Shadowsocks</strong> 服务器信息:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/shadowsocksX-NG-server-config.png" alt="image"></p><p>点击确定后保存配置。再次点击<strong>纸飞机</strong>图标,选择 <strong>PAC自动模式</strong> 或 <strong>全局模式</strong>,然后点击打开 <strong>Shadowsocks</strong> 启动本客户端,之后就可以愉快在 <strong>Mac</strong> 上翻墙啦~</p><h4 id="终端-1"><a href="#终端-1" class="headerlink" title="终端"></a>终端</h4><p>使用过程中不知道你够不够细心,因为完成上述配置后,浏览器确实可以成功访问外网,但是终端却仍然无法翻墙,这里推荐一款 <strong>Mac</strong> 下终端翻墙利器:<a href="https://www.google.com.hk/search?q=privoxy&oq=privoxy&aqs=chrome.0.0j69i61j0l4.2324j0j7&sourceid=chrome&ie=UTF-8" target="_blank" rel="noopener"><strong>privoxy</strong></a>,原理就不讲了,直接上配置教程:</p><p><strong>privoxy</strong> 安装:<br>很简单,用 <strong>brew</strong> 安装即可:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ brew install privoxy</span><br></pre></td></tr></table></figure></p><p><strong>privoxy</strong> 配置:<br>打开配置文件 <strong>/usr/local/etc/privoxy/config</strong>,加入下面两个配置项:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">listen-address 0.0.0.0:8118</span><br><span class="line">forward-socks5 / localhost:1086 .</span><br></pre></td></tr></table></figure></p><p>第一行设置privoxy监听任意IP地址的 <strong>8118</strong> 端口。第二行设置本地socks5代理客户端端口 <strong>1086</strong>,可以通过 <strong>ShadowsocksX-NG</strong> 的设置页面确定,见下图:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/shadowsocksX-NG-settings.png" alt="image"></p><p><strong>privoxy</strong> 启动:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo /usr/local/sbin/privoxy /usr/local/etc/privoxy/config</span><br></pre></td></tr></table></figure></p><p>查看是否启动成功:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ netstat -na | grep 8118</span><br></pre></td></tr></table></figure></p><p>查询结果如下则表明启动成功:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/ladder/privoxy-listen.png" alt="image"></p><p>如果没有,可以查看日志信息,判断哪里出了问题。打开配置文件找到 <strong>logdir</strong> 配置项,查看 <strong>log</strong> 文件。</p><p><strong>privoxy</strong> 使用:<br>在命令行终端中输入如下命令后,该终端即可翻墙了:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ export http_proxy='http://localhost:8118'</span><br><span class="line">$ export https_proxy='http://localhost:8118'</span><br></pre></td></tr></table></figure></p><p>他的原理是讲socks5代理转化成http代理给命令行终端使用。</p><p>如果不想用了取消即可:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ unset http_proxy</span><br><span class="line">$ unset https_proxy</span><br></pre></td></tr></table></figure></p><p>如果关闭终端窗口,功能就会失效,如果需要代理一直生效,则可以把上述两行代码添加到 <strong>~/.bash_profile</strong> 文件最后(或任意<strong>Shell</strong>配置文件,我用的就是 <strong>.zshrc</strong>):<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">vim ~/.bash_profile</span><br><span class="line"># 将以下内容追加到配置文件末尾</span><br><span class="line">export http_proxy='http://localhost:8118'</span><br><span class="line">export https_proxy='http://localhost:8118'</span><br></pre></td></tr></table></figure></p><p>使配置立即生效:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ source ~/.bash_profile # source ~/.zshrc</span><br></pre></td></tr></table></figure></p><p>还可以在 <strong>~/.bash_profile(或我的.zshrc)</strong> 里加入开关函数,使用起来更方便<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">function proxy_off(){</span><br><span class="line"> unset http_proxy</span><br><span class="line"> unset https_proxy</span><br><span class="line"> echo -e "已关闭代理"</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">function proxy_on() {</span><br><span class="line"> export no_proxy="localhost,127.0.0.1,localaddress,.localdomain.com"</span><br><span class="line"> export http_proxy="http://127.0.0.1:8118"</span><br><span class="line"> export https_proxy=$http_proxy</span><br><span class="line"> echo -e "已开启代理"</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>最后,<strong>Mac</strong>下终端翻墙的流程为:</p><blockquote><p>打开 ShadowsocksX-NG -> 启动 privoxy -> 执行命令:proxy_on -> 终端成功翻墙</p></blockquote><p>自此,全文终。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><hr><p>[1]<a href="http://www.tyrion.wang/2017/02/04/VPN%E6%90%AD%E5%BB%BA-%E4%BA%9A%E9%A9%AC%E9%80%8AEC2-Shadowsocks/" target="_blank" rel="noopener">VPN搭建 - Amazon EC2 + Shadowsocks</a><br>[2]<a href="https://vc2tea.com/whats-shadowsocks/" target="_blank" rel="noopener">写给非专业人士看的Shadowsocks简介</a><br>[3]<a href="https://loggerhead.me/posts/shadowsocks-yuan-ma-fen-xi-xie-yi-yu-jie-gou.html" target="_blank" rel="noopener">Shadowsocks源码分析——协议和结构</a><br>[4]<a href="http://celerysoft.github.io/2016-01-15.html" target="_blank" rel="noopener">使用亚马逊AWS搭建免费的Shadowsocks服务器</a><br>[5]<a href="http://blog.csdn.net/xienaoban/article/details/54772942" target="_blank" rel="noopener">我的Linux入门之路 - 02.Shadowsocks-Qt5配置</a><br>[6]<a href="https://www.mystery0.vip/2017/01/12/Ubuntu%E4%BD%BF%E7%94%A8Shadowsocks-qt5%E7%A7%91%E5%AD%A6%E4%B8%8A%E7%BD%91/" target="_blank" rel="noopener">Ubuntu使用Shadowsocks-qt5科学上网</a><br>[7]<a href="http://www.cashqian.net/blog/001486989831982332565298e4942a2bb8f56b08f9d2475000" target="_blank" rel="noopener">Mac命令行终端下使用shadowsocks翻墙</a><br>[8]<a href="https://github.com/shadowsocks/shadowsocks/wiki/Ports-and-Clients#windows%20SS%E5%85%A8%E5%B9%B3%E5%8F%B0%E5%90%84%E4%B8%AA%E7%89%88%E6%9C%AC" target="_blank" rel="noopener">各版本Shadowsocks-GitHub</a><br>[9]<a href="http://blog.leanote.com/post/sxdeveloper/Ubuntu%E4%B8%8B%E8%AE%BE%E7%BD%AEShadowsocks%E7%9A%84%E9%9D%9E%E5%85%A8%E5%B1%80%E4%BB%A3%E7%90%86%EF%BC%88PAC%E8%87%AA%E5%8A%A8%E4%BB%A3%E7%90%86%EF%BC%89" target="_blank" rel="noopener">Ubuntu下设置Shadowsocks的非全局代理(PAC自动代理)</a></p>]]></content>
<categories>
<category> Operating System </category>
</categories>
<tags>
<tag> VPN </tag>
<tag> Shadowsocks </tag>
<tag> Amazon EC2 </tag>
<tag> Proxychain </tag>
<tag> Privoxy </tag>
<tag> SwitchyOmega </tag>
</tags>
</entry>
<entry>
<title>Ubuntu + GitHub Pages + Hexo 搭建个人博客</title>
<link href="/2018/01/25/OS/Installation/Ubuntu/blog-github-hexo/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><hr><p>本博客站点采用 <strong>GitHub Pages</strong> + <strong>Hexo</strong> 框架搭建,其中 <a href="https://pages.github.com/" target="_blank" rel="noopener"><strong>GitHub Pages</strong></a> 作为内容托管平台,<a href="https://hexo.io/zh-cn/index.html" target="_blank" rel="noopener"><strong>Hexo</strong></a> 是一款静态博客框架,提供了多种多样的主题与插件,使得用户可以对个人站点进行个性化定制及管理,支持归档、分类、标签、搜索、资源管理等基本功能。用户可以在本地的博客项目中新建 <a href="http://wowubuntu.com/markdown/" target="_blank" rel="noopener"><strong>Markdown</strong></a> 文档编辑博文内容,并利用 <strong>Hexo</strong> 提供的命令行工具将 <strong>Markdown</strong> 文档转换为个人网络站点所展示的 <strong>HTML</strong> 页面。本文详细介绍了博客框架的配置全记录。<br><a id="more"></a></p><h2 id="GitHub"><a href="#GitHub" class="headerlink" title="GitHub"></a>GitHub</h2><hr><p><strong><a href="https://pages.github.com/" target="_blank" rel="noopener">Github Pages</a></strong> 是 <strong>GitHub</strong> 公司提供的免费的静态网站托管服务,用起来方便而且功能强大,不仅没有空间限制,还可以绑定自己的域名。<strong>Git</strong> 及 <strong>GitHub</strong> 的配置与使用就不多说了,将博客托管到 <strong>GitHub</strong> 上,首先需要新建仓库,名称必须为:</p><blockquote><p><strong>username.github.io</strong></p></blockquote><p><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/blog-github-hexo/sundongxu.github.io.png" alt="image"></p><p>这是特殊的命名约定,创建完毕后可以通过 <strong><a href="dongdongdong.me">http://username.github.io</a></strong> 来访问你的个人主页。</p><p>特别提醒:个人主页的网站内容是发布在master分支下的。</p><h2 id="Hexo"><a href="#Hexo" class="headerlink" title="Hexo"></a>Hexo</h2><hr><p><strong>Hexo</strong> 出自台湾大学生 <strong>tommy351</strong> 之手,基于 <strong>Node.js</strong>,其编译上百篇文字只需要几秒,并且简单易于配置,很适合搭建个人站点。<strong>Hexo</strong> 生成的静态网页可以直接部署到 <strong>GitHub Pages</strong> 等平台上。</p><h3 id="Nodejs"><a href="#Nodejs" class="headerlink" title="Nodejs"></a>Nodejs</h3><p>编译最新版本 <strong>Nodejs (9.5.0)</strong>:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">$ wget https://nodejs.org/dist/v9.5.0/node-v9.5.0.tar.gz</span><br><span class="line">$ tar zxvf node-v9.5.0.tar.gz</span><br><span class="line">$ cd node-v9.5.0</span><br><span class="line">$ ./configure --prefix=/usr/local/node</span><br><span class="line">$ make -j</span><br><span class="line">$ sudo make install</span><br></pre></td></tr></table></figure></p><p>配置环境变量:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">$ vim .zshrc</span><br><span class="line"># 将以下内容添加到末尾</span><br><span class="line">export NODE_HOME=/usr/local/node</span><br><span class="line">export PATH=$PATH:$NODE_HOME/bin</span><br><span class="line">$ source .zshrc</span><br><span class="line">$ node -v # v9.5.0</span><br><span class="line">$ npm -v # 5.6.0</span><br></pre></td></tr></table></figure></p><h3 id="NPM"><a href="#NPM" class="headerlink" title="NPM"></a>NPM</h3><p><strong>NPM (Node Package Manager)</strong> 是随同 <strong>Nodejs</strong> 一起安装的包管理工具,能解决 <strong>Nodejs</strong> 代码部署上的很多问题,常见的使用场景有以下几种:</p><ul><li>允许用户从NPM服务器下载别人编写的第三方包到本地使用</li><li>允许用户从NPM服务器下载并安装别人编写的命令行程序到本地使用</li><li>允许用户将自己编写的包或命令行程序上传到NPM服务器供别人使用</li></ul><p><strong>NPM</strong> 安装 <strong>Nodejs</strong> 模块语法如下:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npm install <module_name></span><br></pre></td></tr></table></figure></p><p>安装好之后,模块包就被放在了工程目录下的 <strong>node_modules</strong> 目录中。</p><p><strong> NPM </strong> 的包安装分为 <strong>本地安装 (local)</strong> 和 <strong>全局安装 (global)</strong> ,命令分别如下:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ npm install <module_name> # 本地</span><br><span class="line">$ npm install <module_name> -g # 全局</span><br></pre></td></tr></table></figure></p><p>两种安装方式的对比如下:</p><blockquote><p><strong>本地安装</strong><br>(1)将安装包放在 ./node_modules 下 (运行 npm 命令时所在的目录);<br>(2)可以通过 require() 来引入本地安装的包。</p><p><strong>全局安装</strong><br>(1)将安装包放在 /usr/local/lib 下或者你 node 的安装目录;<br>(2)可以直接在命令行里使用。</p></blockquote><p>其它常用 <strong>NPM</strong> 命令如下:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">$ npm uninstall <module_name> # 卸载</span><br><span class="line">$ npm update <module_name> # 更新</span><br><span class="line">$ npm search <module_name> # 搜索</span><br><span class="line">$ npm list <module_name> # 查看版本号</span><br><span class="line">$ npm list -g # 查看所有全局安装的模块</span><br></pre></td></tr></table></figure></p><p>国内直接使用 <strong>NPM</strong> 的官方镜像是非常慢的,推荐使用淘宝 <strong>NPM</strong> 镜像,同步频率为10分钟一次,更换镜像源命令如下:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npm install -g cnpm --registry=https://registry.npm.taobao.org</span><br></pre></td></tr></table></figure></p><p>模块的描述文件:<strong>package.json</strong>,它位于各模块的目录下,用于定义包的相关属性,部分字段说明如下:</p><ul><li>name - 包名</li><li>version - 包版本号</li><li>description - 包的描述</li><li>homepage - 包的官网 url</li><li>author - 包的作者姓名</li><li>contributors - 包的其他贡献者姓名</li><li>dependencies - 依赖包列表。如果依赖包没有安装,npm 会自动将依赖包安装在 node_module 目录下</li><li>repository - 包代码存放的地方的类型,可以是 git 或 svn,git 可在 Github 上</li><li>main - main 字段指定了程序的主入口文件,require(‘moduleName’) 就会加载这个文件。这个字段的默认值是模块根目录下面的 index.js</li><li>keywords - 关键字</li></ul><h3 id="Hexo-1"><a href="#Hexo-1" class="headerlink" title="Hexo"></a>Hexo</h3><p>使用 <strong>NPM</strong> 安装 <strong>Hexo</strong>:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npm install -g hexo-cli # 可能需要 sudo 权限</span><br></pre></td></tr></table></figure></p><h4 id="建站"><a href="#建站" class="headerlink" title="建站"></a>建站</h4><p><strong>Hexo</strong> 安装完成后,初始化博客目录,输入:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ hexo init blog</span><br><span class="line">$ cd blog</span><br><span class="line">$ npm install</span><br></pre></td></tr></table></figure></p><p><strong>Hexo</strong> 会在指定文件夹中新建需要的文件,完成后,该文件夹下的目录文件说明如下:</p><blockquote><p><strong>_config.yml</strong>:站点配置文件<br><strong>package.json</strong>:Hexo应用程序信息<br><strong>scaffolds</strong>:模板文件夹,根据此文件夹下的.md文档新建页面、文章、草稿<br><strong>source</strong>:用户资源文件夹,_post保存博文文档,Markdown 和 HTML 文件会被解析并放到 public 文件夹,而其他文件会被拷贝过去。分类、标签、关于页面模板分别放在 categories、tags 和 about 下<br><strong>themes</strong>:主题文件夹,Hexo会根据主题来生成静态页面</p></blockquote><h4 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h4><p>你可以在 <strong>_config.yml</strong> 中修改大部分站点配置,详见<a href="https://hexo.io/zh-cn/docs/configuration.html" target="_blank" rel="noopener"><strong>官方文档-Hexo配置</strong></a></p><h4 id="命令"><a href="#命令" class="headerlink" title="命令"></a>命令</h4><p>基本 <strong>Hexo</strong> 命令如下:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ hexo new "post_name" # 新建博文,.md文档生成保存在source/_post目录下</span><br><span class="line">$ hexo generate/g # 载入全部.md文件,生成可供浏览器展示的HTML静态页面</span><br><span class="line">$ hexo server/s # 将本机作为托管服务器,在本地启动站点,可通过 localhost:4000 访问博客</span><br></pre></td></tr></table></figure></p><p>在某些情况(尤其是更换主题后),如果发现对站点的更改无论如何也不生效,可能需要运行 <strong>clean</strong> 命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo clean # 清除缓存文件 (db.json) 和已生成的静态文件 (public)</span><br></pre></td></tr></table></figure></p><p>最终博客文件是要被托管到 <strong>GitHub</strong> 上的,确认已经创建名为 <strong>username.github.io</strong> 的仓库后,执行:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npm install hexo-deployer-git --save </span><br></pre></td></tr></table></figure> </p><p>安装好插件后编辑 <strong>_config.yml</strong> 文件,翻到最后,找到 <strong>deploy</strong> 字段,改成如下格式:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">deploy:</span><br><span class="line"> type: git</span><br><span class="line"> repo: https://github.com/username/username.github.io.git</span><br><span class="line"> branch: master</span><br></pre></td></tr></table></figure></p><p>配置完成后,<strong>Hexo</strong> 框架下的博客文件就可以通过以下命令轻松部署并发布到 <strong>GitHub</strong> 对应仓库中的 <strong>master</strong> 分支上:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo deploy/d</span><br></pre></td></tr></table></figure></p><p>到这里,你的博客就已经能够被搜索引擎索引到,或直接访问 <strong><a href="http://username.github.io" target="_blank" rel="noopener">http://username.github.io</a></strong>,供人查阅:</p><p>除开官网上的基本配置,<strong>Hexo</strong> 更是提供了丰富的个性化选项,例如 更换主题、流量统计、评论分享、站内搜索 … 太适合折腾了有木有!</p><p>不过,“弱水三千,只取一瓢饮”,点到为止即可,毕竟写博客的初衷一直都是记录、积累和总结知识呀。</p><p>当然啦,伸手党也是要满足的,<a href="https://segmentfault.com/a/1190000009544924" target="_blank" rel="noopener"><strong>这篇</strong></a>拿去不谢~</p><h2 id="图片托管"><a href="#图片托管" class="headerlink" title="图片托管"></a>图片托管</h2><hr><p>Hexo文章中的图片,当然可以放到本地,在博文中通过相对路径引用,然后一起部署到github中,这样完全没有问题。然而 <strong>GitHub Pages</strong> 空间毕竟有限(貌似只有300M),另外图片的管理太混乱了,一些原创的图片可能被盗链。七牛作为国内顶尖的 <strong>CDN</strong> 云存储商,有以下几个重要优势:</p><ul><li>在国内很稳定,我们公司也是选择七牛来提供云存储的</li><li>免费提供10G存储空间,和每月10G下载流量,完全够用</li><li>Hexo有七牛的插件,使用起来也是相当的方便</li></ul><h3 id="申请空间"><a href="#申请空间" class="headerlink" title="申请空间"></a>申请空间</h3><p>点击<a href="https://portal.qiniu.com/signup?code=3lbz03l9r6c7m" target="_blank" rel="noopener"><strong>此处</strong></a>申请七牛账号,随后点击 对象存储(立即添加) —> 创建公开空间,记录七牛为该存储空间分配的域名,比如我的空间名为 <strong>dongdongdong-blog</strong>,域名为 <strong>oyj8xdeki.bkt.clouddn.com</strong>。</p><p>点击右上角 <strong>个人面板</strong> —> 密钥管理,复制当前使用的 <strong>AK (AccessKey)</strong> 和 <strong>SK (SecretKey)</strong>,待会儿设置插件时会用到。</p><h3 id="安装插件"><a href="#安装插件" class="headerlink" title="安装插件"></a>安装插件</h3><p><strong>Hexo</strong> 安装七牛插件:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ npm install hexo-qiniu-sync --save</span><br></pre></td></tr></table></figure></p><p>将插件配置信息写入站点配置文件 <strong>_config.yml</strong>:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">plugins: </span><br><span class="line"> -hexo-qiniu-sync</span><br><span class="line">qiniu:</span><br><span class="line"> offline: false</span><br><span class="line"> sync: true</span><br><span class="line"> bucket: dongdongdong-blog(存储空间名称)</span><br><span class="line"> access_key: 上一步记录的 AK 值</span><br><span class="line"> secret_key: 上一步记录的 SK 值</span><br><span class="line"> dirPrefix: </span><br><span class="line"> urlPrefix: oyj8xdeki.bkt.clouddn.com(空间域名)</span><br><span class="line"> local_dir: cdn(本地目录名称)</span><br><span class="line"> update_exist: true</span><br><span class="line"> images:</span><br><span class="line"> folder: images</span><br><span class="line"> extend:</span><br><span class="line"> js:</span><br><span class="line"> folder: js</span><br><span class="line"> css: </span><br><span class="line"> folder: css</span><br></pre></td></tr></table></figure></p><h3 id="配置目录"><a href="#配置目录" class="headerlink" title="配置目录"></a>配置目录</h3><p>图片一般都先保存在本地,然后再通过上述插件同步到七牛云。</p><p>本地目录名称需要与配置项 <strong>local_dir</strong> 一致,我将其命名为 <strong>cdn</strong>,并与博客主目录下的 source 文件夹平级,用于存放需要上传到七牛的资源。</p><p>在 <strong>cdn</strong> 目录下创建子目录:<strong>css</strong>、<strong>images</strong>、<strong>js</strong>,与 <strong>image/js/css</strong> 字段的子参数 <strong>folder</strong> 保持一致,待上传的 <strong>css</strong>、<strong>图片</strong>、<strong>js</strong> 文件应该存储到相应子目录。</p><h3 id="引用资源"><a href="#引用资源" class="headerlink" title="引用资源"></a>引用资源</h3><p>以图片引用为例,这也是最常见的情形。如果你想引用存储在 <strong>cdn/images</strong> 下的图片 <strong>demo.jpg</strong>,只需在文章中插入:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">![image](https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/demo.jpg) # 如果 cdn/image 目录下有更深层子目录,则加上后面的路径</span><br></pre></td></tr></table></figure></p><p>生成网页时将被解析为:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><img src="http://oyj8xdeki.bkt.clouddn.com/images/demo.jpg">·</span><br></pre></td></tr></table></figure></p><p>将引用实例概括为:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">{% qnimg ImgFile %}</span><br></pre></td></tr></table></figure></p><p>注意到上面七牛插件配置文件的 <strong>urlPrefix</strong> 和 <strong>images</strong> 的 <strong>folder</strong> 子参数,不难发现 <strong>图片外链地址</strong> 的生成规则为:</p><blockquote><p>urlPrefix + / + images.folder + / + ImgFile</p></blockquote><p>定义文件在 <strong>cdn</strong> 目录下的 <strong>相对路径</strong> 为 <strong>FilePath</strong> ,即文件 <strong>cdn/demo.jpg</strong> 的 <strong>FilePath</strong> 为 <strong>demo.jpg</strong>,<strong>cdn/images/test/01.png</strong> 的 <strong>FilePath</strong> 为 <strong>images/test/01.png</strong>。则实际 <strong>上传到七牛的文件外链地址</strong> 为:</p><blockquote><p>AK/SK所指向用户的绑定域名 + / + dirPrefix + / + FilePath</p></blockquote><p>只有当生成的图片外链地址与实际上传到七牛的文件外链地址一致时,引用的图片才能正确显示。</p><h3 id="同步上传"><a href="#同步上传" class="headerlink" title="同步上传"></a>同步上传</h3><p>每当将新资源文件放入需要同步的文件夹 <strong>cdn</strong> 中后,执行 <strong>hexo s</strong> 即启动站点服务器即可完成资源上传到七牛云,但有时候会莫名失效…</p><p>若上述命令没能成功上传,执行:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo qiniu sync</span><br></pre></td></tr></table></figure></p><p>一定可以将资源同步到云端。</p><h2 id="多机同步"><a href="#多机同步" class="headerlink" title="多机同步"></a>多机同步</h2><hr><p>执行:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo deploy/d</span><br></pre></td></tr></table></figure></p><p>即可将本地博客内容发布到 <strong>GitHub</strong> 上,其中 <strong>仓库</strong> 与 <strong>分支名</strong> 由 <strong>_config.yml</strong> 中的 <strong>deploy</strong> 字段的子参数 <strong>repo</strong> 和 <strong>branch</strong> 显式指定。</p><p>此时在 <strong>GitHub</strong> 个人仓库 <strong>username.github.io</strong> 的 <strong>master</strong>分支的页面即可看到上传记录:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/blog-github-hexo/sundongxu.github.io-publish.png" alt="image"></p><p>不难发现这个博客仓库是不包含原始文件的,比如博文对应的 <strong>.md</strong> 文件,而该文件都只保存在配置 <strong>Hexo</strong> 的机器本地,而上传到 <strong>GitHub</strong> 的只是转换渲染过后的 <strong>.html</strong> 网页文件。那么这就带来一个问题:如何能在原始机器以外的机器上继续编辑博文(.md文档)呢?</p><p>这就是一个多设备同步的问题,利用版本控制,思路如下:</p><ol><li>电脑A搭建好 <strong>GitHub</strong> 博客</li><li>编译好的静态网页提交到主分支 <strong>master</strong>,提供访问</li><li>创建一个分支 <strong>hexo</strong>,保存网站的源文件</li><li>在电脑B上拉取 <strong>hexo</strong> 分支源文件,做更新博客操作</li><li>编译博客,静态文件提交到主分支 <strong>master</strong>,源文件提交到 <strong>hexo</strong> 分支</li><li>回到电脑A,同步 <strong>hexo</strong> 分支源文件到本地,开始更新博客</li></ol><p>下面介绍每一步的具体操作:<br>第一步:在电脑A处,创建名为 <strong>username.github.io</strong> 的 <strong>GitHub</strong> 仓库;</p><p>第二步:编辑 <strong>_config.yml</strong> 设置 <strong>deploy</strong> 参数的子参数 <strong>repo</strong> 和 <strong>branch</strong> 分别为第一步中创建的仓库名和主分支 <strong>master</strong>;</p><p>第三步:在 <strong>GitHub</strong> 仓库中创建新分支 <strong>hexo</strong>,并将部分站点源文件 (如source文件夹,最好写个 <strong>.gitignore</strong> 文件) 提交到该分支,在博客根目录下执行命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">$ rm -rf themes/.git # Git项目内不能再包含Git子项目</span><br><span class="line">$ git init</span><br><span class="line">$ git checkout -b hexo # 创建本地分支hexo </span><br><span class="line">$ git add .</span><br><span class="line">$ git commit -m "create a new branch for coordination among multiple devices" </span><br><span class="line">$ git push origin hexo</span><br></pre></td></tr></table></figure></p><p>第四步:在电脑B处拉取分支hexo,做更新博客操作,先需搭建环境:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ git clone -b hexo https://github.com/username/username.github.io.git</span><br><span class="line">$ cd username.github.io</span><br><span class="line">$ npm install # 安装依赖</span><br></pre></td></tr></table></figure></p><p>此后就可以在电脑B上编辑更新博文了。</p><p>第五步:编译博客,将静态文件发布到主分支 <strong>master</strong> 上,源文件提交到分支 <strong>hexo</strong> 上:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">$ hexo clean && hexo g && hexo d</span><br><span class="line">$ git add .</span><br><span class="line">$ git commit -m "message" # 源文件提交到hexo分支上面</span><br><span class="line">$ git pull origin hexo # 先拉取原来GitHub的hexo分支上的源文件到本地,进行合并</span><br><span class="line">$ git push origin hexo # 比较解决前后版本冲突后,push源文件到GitHub的hexo分支</span><br></pre></td></tr></table></figure></p><p>第六步:再次回到电脑A进行博文编辑工作,同步hexo分支源文件到本地,进行合并:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ git pull origin hexo</span><br><span class="line"># 写好博文,重复操作第五步</span><br></pre></td></tr></table></figure></p><p>最后提醒一点,<strong>Git</strong> 带来的版本控制思想除了方便协同工作以外,还间接起到了文件备份的作用,所以无论在何处编辑好分支文件后,都请务必记得将本次修改内容提交到远端分支。对于博客工作而言,你是在正式发布博文 (hexo d) 之前还是之后,将源文件提交到 <strong>hexo</strong> 分支,这顺序不重要,不要忘记这一步就好。</p><h2 id="域名管理"><a href="#域名管理" class="headerlink" title="域名管理"></a>域名管理</h2><hr><p>安装前文搭建好的博客站点,已经可以通过域名 <strong><a href="http://username.github.io" target="_blank" rel="noopener">http://username.github.io</a></strong> 被公网访问到,这个是由 <strong>GitHub Pages</strong> 免费提供的域名服务,当然你还可以通过购买配置后使用付费的专有域名,本博客域名 <a href="dongdongdong.me"><strong>dongdongdong.me</strong></a> 就是我在腾讯云上购买的,新用户或是学生还经常有优惠,第一年使用权都很便宜~</p><p>购买域名后,需进行实名认证,之后还要有一些配置工作,才能使该域名能够被正常解析并被定向至博客站点。</p><p>绑定域名需要在 域名解析服务商 和 <strong>GitHub</strong> 两边都进行操作。</p><ol><li>在域名解析服务商进行个人域名解析,将域名绑定到个人 GitHub Pages。</li><li>同时在 GitHub Pages 需要配置 CNAME 文件重定向到你的域名。</li></ol><p>下面以我的域名 <a href="dongdongdong.me"><strong>dongdongdong.me</strong></a> 为例,具体步骤如下:</p><p><strong>第一步</strong>:在博客的 <strong>source</strong> 下新增一个 <strong>CNAME (一定大写)</strong> 文件,其中只能包含一个顶级域名:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dongdongdong.me</span><br></pre></td></tr></table></figure></p><p>并发布到 <strong>GitHub Pages</strong> 上,完成 <strong>GitHub</strong> 一侧的配置操作:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo clean && hexo g && hexo s</span><br></pre></td></tr></table></figure></p><p>本步骤<strong>替代方案</strong>:直接设置 <strong>GitHub Pages</strong> 的 <strong>Custom Domain (自定义域)</strong>:<br>进入名为 <strong>username.github.io</strong> 的仓库的设置(Setting)页面,在 <strong>Custom Domain</strong> 下面的方框里填入域名:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/blog-github-hexo/setting-custom-domain.png" alt="image"></p><p><strong>第二步</strong>:向你的 <strong>DNS</strong> 配置中添加三条记录:</p><table><thead><tr><th style="text-align:left">主机记录</th><th style="text-align:left">记录类型</th><th style="text-align:left">记录值</th></tr></thead><tbody><tr><td style="text-align:left">@</td><td style="text-align:left">A</td><td style="text-align:left">192.30.252.153</td></tr><tr><td style="text-align:left">@</td><td style="text-align:left">A</td><td style="text-align:left">192.30.252.154</td></tr><tr><td style="text-align:left">www</td><td style="text-align:left">CNAME</td><td style="text-align:left">sundongxu.github.io</td></tr></tbody></table><p>有两种配置 <strong>DNS</strong> 解析记录的方式:<br>(1)<a href="https://cloud.tencent.com/" target="_blank" rel="noopener"><strong>腾讯云</strong></a></p><blockquote><p>腾讯云主页 -> 控制台 -> 域名管理 -> 选择指定域名 -> 域名解析 -> 添加记录</p></blockquote><p><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/blog-github-hexo/dns-tencent-cloud.png" alt="image"></p><p>(2)<a href="https://www.dnspod.cn/" target="_blank" rel="noopener"><strong>DNSPOD</strong></a></p><blockquote><p>DNSPOD主页 -> 管理控制台 -> 已开通的服务 -> 域名解析 -> 选择指定域名 -> 添加记录</p></blockquote><p><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/blog-github-hexo/dns-dnspod.png" alt="image"></p><p>配置完成后你会发现,无论按照哪一种方式添加 <strong>DNS</strong> 记录,都会在另一种方式的网页上被同步添加,其实腾讯云与<strong>DNSPOD</strong>早就打通数据通道啦。</p><p>第三步:等待 <strong>DNS</strong> 配置生效。<br>对 <strong>DNS</strong> 的配置不是立即生效的,过10分钟左右再去访问你的域名看看是否成功定向到你的 <strong>GitHub Pages</strong> 个人站点。</p><h2 id="参考资源"><a href="#参考资源" class="headerlink" title="参考资源"></a>参考资源</h2><hr><p>[1]<a href="https://hexo.io/zh-cn/docs/index.html" target="_blank" rel="noopener">Hexo文档-配置</a><br>[2]<a href="http://magicse7en.github.io/2016/03/06/ubuntu-github-hexo-blog-setup/" target="_blank" rel="noopener">Ubuntu + Github + Hexo搭建blog小记-magicse7en</a><br>[3]<a href="https://www.jianshu.com/p/ecd51e8ef2fa" target="_blank" rel="noopener">通过Hexo在GitHub上搭建博客</a><br>[4]<a href="https://zhuanlan.zhihu.com/p/26625249" target="_blank" rel="noopener">GitHub+Hexo搭建个人网站详细教程-知乎专栏</a><br>[5]<a href="https://yuchen-lea.github.io/2016-01-21-use-qiniu-store-file-for-hexo/#fn.1" target="_blank" rel="noopener">使用七牛为Hexo存储图片等资源</a><br>[6]<a href="https://zhangangs.github.io/2017/05/22/%E5%A4%9A%E8%AE%BE%E5%A4%87%E6%9B%B4%E6%96%B0hexo%E6%90%AD%E5%BB%BA%E7%9A%84github%E5%8D%9A%E5%AE%A2/" target="_blank" rel="noopener">多设备更新hexo搭建的GitHub博客</a><br>[7]<a href="https://www.zhihu.com/question/31377141" target="_blank" rel="noopener">GitHub怎么绑定自己的域名?</a></p>]]></content>
<categories>
<category> Operating System </category>
</categories>
<tags>
<tag> Blog </tag>
<tag> Ubuntu </tag>
<tag> GitHub </tag>
<tag> Hexo </tag>
</tags>
</entry>
<entry>
<title>Ubuntu 16.04 最好用的 Vim 配置</title>
<link href="/2018/01/22/OS/Installation/Ubuntu/vim/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><hr><p>关于<strong>Vim</strong> ,不消多说,熟练了就是Linux上最好的文本代码编辑器,丰富的命令和强大的插件支持几乎能满足即使最挑剔的那一部分人的需求。本文详细介绍了安装 <strong>Vim</strong> 的正确姿势以及一款超强大的 <strong>Vim</strong> 插件 <strong>VimPlus</strong> 的配置过程,更多的 <strong>Vim</strong> 使用技巧会在后续的博文随本文学习总结的进度同步推出。<br><a id="more"></a></p><blockquote><p>VIM is the God of editors, EMACS is God’s editor.<br>EMACS is actually an OS which pretends to be an editor.</p></blockquote><p>以上,大概是你能听到的关于编辑器的最高评价了。</p><p>回到本文的主角 <strong>Vim</strong> ,接触它到现在也有几年了,复杂繁多的命令着实让人经历了相当痛苦的一段磨合期。然而,本文志不在普及 <strong>Vim</strong> 的使用技巧,而是从其安装过程入手,教会你轻松配置一个更友好的 <strong>Vim</strong> 工作环境。</p><p>之前配置 <strong>Vim</strong> 都是在网上找别人写好的配置文件,比如 <strong>.vimrc</strong>。但是别人配置的始终都不能够满足自己的需求,比如:强大的 C/C++ 代码提示补全功能、头文件/源文件切换、静态代码分析等功能,直到 <strong>VimPlus</strong>,<a href="http://www.cnblogs.com/highway-9/" target="_blank" rel="noopener"><strong>作者</strong></a>自己归纳了一些vim的插件,然后做成了一键安装程序,给广大 <strong>Vimers</strong>带来了福音。</p><h2 id="Vim安装"><a href="#Vim安装" class="headerlink" title="Vim安装"></a>Vim安装</h2><hr><p><strong>VimPlus</strong> 安装脚本中已经默认会帮你重新拉取GitHub上最新的 <strong>Vim</strong> 源码并自动安装,其中 Ubuntu 16.04 系统安装 <strong>Vim</strong> 步骤如下:<br>(1)安装 <strong>Vim</strong> 相关库依赖<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ sudo apt-get install -y ctags build-essential cmake python-dev python3-dev fontconfig</span><br><span class="line">$ sudo apt-get install -y libncurses5-dev libgnome2-dev libgnomeui-dev libgtk2.0-dev libatk1.0-dev libbonoboui2-dev libcairo2-dev libx11-dev libxpm-dev libxt-dev python-dev python3-dev ruby-dev lua5.1 lua5.1-dev </span><br></pre></td></tr></table></figure></p><p>(2)卸载 <strong>Vim</strong> 相关软件<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ sudo apt-get -y remove vim vim-runtime gvim # 卸载系统自带或通过 apt-get 安装的 Vim</span><br><span class="line">$ sudo apt-get -y remove vim-tiny vim-common vim-gui-common vim-nox</span><br></pre></td></tr></table></figure></p><p>(3)删除 <strong>Vim</strong> 相关文件夹<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ sudo rm -rf ~/vim</span><br><span class="line">$ sudo rm -rf /usr/share/vim/vim74</span><br><span class="line">$ sudo rm -rf /usr/share/vim/vim80</span><br></pre></td></tr></table></figure></p><p>(4)拉取 <strong>Vim</strong> GitHub源码并编译安装<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">$ git clone https://github.com/vim/vim.git ~/vim</span><br><span class="line">$ cd ~/vim</span><br><span class="line">$ ./configure --with-feature-huge --enable-multibyte --enable-rubyinterp --enable-pythoninterp --with-python-config-dir=/usr/lib/python2.7/config-x86_64-linux-gnu</span><br><span class="line"> --enable-perlinterp --enable-luainterp --enable-gui=gtk2 --enable-cscope --prefix=/usr</span><br><span class="line">$ make VIMRUNTIMEDIR=/usr/share/vim/vim80</span><br><span class="line">$ sudo make install </span><br><span class="line">$ cd - </span><br></pre></td></tr></table></figure></p><p>其中 <strong>configure</strong> 脚本执行参数说明如下:</p><blockquote><p>–with-features=huge:支持最大特性<br>–enable-rubyinterp:启用Vim对ruby编写的插件的支持<br>–enable-pythoninterp:启用Vim对python编写的插件的支持<br>–enable-luainterp:启用Vim对lua编写的插件的支持<br>–enable-perlinterp:启用Vim对perl编写的插件的支持<br>–enable-multibyte:多字节支持,可以在Vim中输入中文<br>–enable-cscope:Vim对cscope支持<br>–enable-gui=gtk2:gtk2支持,也可以使用gnome,表示生成gvim<br>–with-python-config-dir=/usr/lib/python2.7/config-i386-linux-gnu/:指定 <strong>python</strong> 路径,注意这里要修改成本机的 <strong>python</strong> 相关路径<br>–prefix=/usr:编译安装路径,也可以自己指定</p></blockquote><h2 id="VimPlus安装"><a href="#VimPlus安装" class="headerlink" title="VimPlus安装"></a>VimPlus安装</h2><hr><p>以上,<strong>Vim</strong> 安装完成,接下来是 <strong>VimPlus</strong> 插件的安装,根据 <strong>install.sh</strong> 安装脚本,<strong>VimPlus</strong> 使用 <a href="https://github.com/VundleVim/Vundle.vim" target="_blank" rel="noopener"><strong>Vundle</strong></a> 来管理 <strong>Vim</strong> 插件。</p><p><strong>Vim</strong> 有三种类型的插件:<br>(1)Github上 vim-scripts 仓库的插件;<br>(2)Github上非 vim-scripts 仓库的插件;<br>(3)不在Github上的插件。</p><p>而对于不同的插件,<strong>vundle</strong> 自动管理和下载插件的时候,有不同的地址填写方法,有如下三类:<br>(1)在Github上vim-scripts用户下的仓库,只需要写出repos(仓库)名称;<br>(2)在Github其他用户下的repos, 需要写出”用户名/repos名”;<br>(3)不在Github上的插件,需要写出git全路径。</p><p>之后想要为 <strong>Vim</strong> 添加新插件,只需编辑 <strong>.vimrc</strong> 文件,在下图对应位置处添加插件的下载地址路径:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/vim/vimplus-vimrc-vundle.png" alt="image"></p><p>保存 <strong>.vimrc</strong> 文件后,有以下两种方式开始下载安装插件:<br>(1)运行 vim ,再运行 :PluginInstall:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ vim</span><br><span class="line">: PluginInstall</span><br></pre></td></tr></table></figure></p><p>(2)命令行执行或编辑以下内容到脚本中执行:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ vim -c "PluginInstall" -c "q" -c "q"</span><br><span class="line"># 或</span><br><span class="line">$ vim -c "PluginInstall" -c "qall" </span><br></pre></td></tr></table></figure></p><p>开始安装插件时应该是这个样子:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/vim/vimplus-plugins.png" alt="image"></p><p>其中要提一下的是 <a href="http://valloric.github.io/YouCompleteMe/" target="_blank" rel="noopener"><strong>YouCompleteMe</strong></a> 这个插件,用于 <strong>Vim</strong> 自动补全,可由于某种不可描述的缘故,下载安装它非常慢。着急的话,可以采用下面介绍的离线安装方法:<br>将 <strong>vimrc</strong> 中 <strong>YouCompleteMe</strong>那一行注释掉,之后再从下面的地址下载后进行离线安装:<br><a href="/2018/01/22/OS/Installation/Ubuntu/vim/YouCompleteMe.tar.gz" title="YouCompleteMe下载">YouCompleteMe下载</a></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">$ cd ~/Downloads # 浏览器下载后默认放在此路径下</span><br><span class="line">$ mv YouCompleteMe.tar.gz ~/.vim/bundle</span><br><span class="line">$ cd ~/.vim/bundle</span><br><span class="line">$ tar zxvf YouCompleteMe.tar.gz</span><br><span class="line">$ cd YouCompleteMe</span><br><span class="line">$ ./install.py --clang-completer</span><br></pre></td></tr></table></figure><p>到这一步,安装已经完成,你会发现~目录有两个文件,一个是vim的配置文件 <strong>.vimrc</strong>,一个是 <strong>YouCompleteMe</strong> 的配置文件 <strong>.ycm_extra_conf.py</strong>,一般来说新建一个 main.cpp 来写C、C++程序来说是没有问题的,都会有语法补全,当你需要写一些项目并涉及到第三方库时,就需要修改 <strong>.ycm_extra_conf.py</strong> 了,具体步骤如下:<br>(1)将 <strong>.ycm_extra_conf.py</strong> 拷贝的项目的根目录。<br>(2)更改 <strong>.ycm_extra_conf.py</strong> 里面的 <strong>flags</strong>变量,添加 <strong>第三方库</strong> 路径和<strong>工程子目录</strong> 路径。</p><p>桌面版linux使用 <a href="https://github.com/ryanoasis/vim-devicons" target="_blank" rel="noopener"><strong>vim-devicons</strong></a> 插件会出现乱码,需要设置终端字体为 <strong>Droid Sans Mono for Powerline Nerd Font Complete</strong> ,使用xshell等工具连接服务器linux的用户就没有必要使用 <strong>vim-devicons</strong> 了,可以在插件目录将 <strong>vim-devicons</strong> 目录删除,不然会导致 <strong>NerdTree</strong> 的缩进有问题。</p><h2 id="VimPlus快捷键"><a href="#VimPlus快捷键" class="headerlink" title="VimPlus快捷键"></a>VimPlus快捷键</h2><hr><p><strong>vim</strong> 的插件需要设置好了快捷键才会发挥它的威力,有些插件的快捷键可以查看各自官网,下面罗列部分插件的快捷键:</p><blockquote><p>(1)显示目录树:F3<br>(2)显示函数、变量、宏定义等:F4<br>(3)显示静态代码分析结果(类型编译器检查语法错误多少个error多少个warning):F5<br>(4).h .cpp 文件快速切换:F2<br>(5)转到声明:, + u<br>(6)转到定义:, + i<br>(7)打开include文件:, + o<br>(8)Buffer切换:Ctrl + P 或 Ctrl + N<br>(9)光标位置切换:Ctrl + O 或 Ctrl + I<br>(10)模糊搜索文件:Ctrl + f<br>(11)注释:gcc/gcap/gc/,ca/,cA<br>(12)DirDiff:DirDiff <dir1> <dir2><br>(13)重复 .<br>(14)前后更换Vim界面主题:F9与F10</dir2></dir1></p></blockquote><h2 id="Vim教程"><a href="#Vim教程" class="headerlink" title="Vim教程"></a>Vim教程</h2><hr><p>入门教程就放 <a href="https://coolshell.cn/articles/5426.html" target="_blank" rel="noopener"><strong>简明 Vim 练级教程 - 酷壳</strong></a></p><p>这里放几张 <strong>Vim</strong> 使用技巧的示意图,便于学习掌握:</p><p>先来张 <strong>Vi/Vim</strong> 键盘图:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/vim/vi-vim-cheat-sheet.png" alt="image"></p><p>英文看不懂?没关系,下面是中文版:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/vim/vi-vim-cheat-sheet-cn.png" alt="image"></p><p>程序员或许更喜欢这样的:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/vim/vim-cheat-sheet-for-programmers-screen.png" alt="image"></p><p>还有这样的:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/vim/vim-cheat-sheet-en-visual.png" alt="image"></p><p>总而言之,<strong>Vim</strong> 是一款功能超乎想象地强大齐全的编辑器。然而,使用者不可能会用到其所有的特性,我们只需掌握基本常用的操作命令,其余的在要使用时再去查阅教程或者攻略即可。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><hr><p>[1]<a href="http://feihu.me/blog/2014/intro-to-vim/#%E4%B8%BA%E4%BB%80%E4%B9%88%E9%80%89%E5%85%B6%E5%AE%83" target="_blank" rel="noopener">跟我一起学Vim—The life-changing Editor</a><br>[2]<a href="http://www.cnblogs.com/highway-9/p/5414465.html" target="_blank" rel="noopener">超级强大的vim配置-vimplus</a><br>[3]<a href="http://www.cnblogs.com/highway-9/p/5984285.html" target="_blank" rel="noopener">超级强大的vim配置-vimplus(续集)</a><br>[4]<a href="http://www.cnblogs.com/burningTheStar/p/6961220.html" target="_blank" rel="noopener">打造一流编辑器vimplus</a></p>]]></content>
<categories>
<category> Operating System </category>
</categories>
<tags>
<tag> Ubuntu </tag>
<tag> Vim </tag>
<tag> VimPlus </tag>
</tags>
</entry>
<entry>
<title>终端利器 Zsh 配置全方略 (Linux与MacOS)</title>
<link href="/2018/01/17/OS/Installation/Ubuntu/zsh/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><hr><p><strong>Zsh</strong> 作为一款 <strong>Terminal</strong> 利器,使用方便,功能强大,社区活跃,并且同时适用于 <strong>Linux</strong> 与 <strong>MacOS</strong> 系统,本文详细介绍了在两种操作系统下 <strong>Zsh</strong> 的安装过程、文件配置及插件功能。<br><a id="more"></a></p><h2 id="Shell"><a href="#Shell" class="headerlink" title="Shell"></a>Shell</h2><hr><p>Shell 是 Linux/Unix 的一个外壳,理解成衣服也行,它负责外界与Linux内核的交互,接收来自用户或其他应用程序的命令,然后把这些命令转化成<strong>内核</strong>能理解的语言,传给内核,内核是真正干活的,干完之后再把结果返回用户或应用程序。</p><p>Linux/Unix提供了很多种Shell,为啥要这么多Shell?那我问你,同类型的衣服你怎么要买那么多件?花色、质地、风格不一样呗。写程序比买衣服复杂多了,而程序员往往负责把复杂的事情搞简单,简单的事情搞复杂。大牛程序员看到不爽的Shell,就会自己重新写一套,慢慢形成了一些标准,常用的Shell有这么几种,sh、bash、csh等,想知道你的系统有几种shell,可以通过以下命令查看:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ cat /etc/shells</span><br></pre></td></tr></table></figure></p><p>由于我已经事先安装好了<strong>Zsh</strong>和<strong>Fish</strong>,所以看起来是这样的:</p><blockquote><p>/bin/sh<br>/bin/dash<br>/bin/bash<br>/bin/rbash<br>/bin/zsh<br>/usr/bin/zsh<br>/usr/bin/fish</p></blockquote><p>目前常用的 Linux 系统和 MacOS 系统的默认 Shell 都是 bash,但是真正强大的 Shell 是深藏不露的 <strong>Zsh</strong>, 这货绝对是飞机中的战斗机,但是之前由于配置过于复杂,所以初期无人问津,很多人跑过来看看 zsh 的配置指南,什么都不说转身就走了。直到有一天,国外有个穷极无聊的程序员开发出了一个能够让人快速上手的zsh项目,叫做<a href="http://ohmyz.sh/" target="_blank" rel="noopener"><strong>oh-my-zsh</strong></a>,GitHub地址在<a href="https://github.com/robbyrussell/oh-my-zsh" target="_blank" rel="noopener"><strong>这里</strong></a>,这玩意差不多就像是《XX天从入门到精通》系列,号称让人神功速成,而且居然是真的…</p><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><hr><p>在Linux系统下,这里以 <strong>Ubuntu</strong> 为例,<strong>Zsh</strong> 安装很方便:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ sudo apt-get install zsh git wget</span><br><span class="line">$ wget --no-check-certificate https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | sh # 下载并安装Zsh</span><br></pre></td></tr></table></figure></p><p>安装 <strong>oh-my-zsh</strong> 可以自动安装也可以手动安装<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"># 自动安装:</span><br><span class="line">$ wget https://github.com/robbyrussell/oh-my-zsh/raw/master/tools/install.sh -O - | sh</span><br><span class="line"># 手动安装</span><br><span class="line">$ git clone git://github.com/robbyrussell/oh-my-zsh.git ~/.oh-my-zsh</span><br><span class="line">$ cp ~/.oh-my-zsh/templates/zshrc.zsh-template ~/.zshrc</span><br></pre></td></tr></table></figure></p><p>而在 <strong>MacOS</strong> 系统中,系统预装了 <strong>Zsh</strong>,那么只需要安装 <strong>oh-my-zsh</strong> 即可,下面的配置及插件使用过程两个系统都相同。</p><h2 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h2><hr><p>安装完成,执行以下命令将 <strong>Zsh</strong> 设置为默认 <strong>Shell</strong>:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"># Ubuntu</span><br><span class="line">$ chsh -s /usr/bin/zsh</span><br><span class="line"># MacOS</span><br><span class="line">$ chsh -s /bin/zsh</span><br></pre></td></tr></table></figure></p><p>注销一次,重新打开终端生效,查看此时使用的 <strong>Shell</strong>:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ echo $SHELL</span><br></pre></td></tr></table></figure><br>显示为 <strong>Zsh</strong> 即可。</p><p><strong>Zsh</strong> 的配置主要集中在用户home目录的 <strong>.zshrc</strong> 里,可以在此处定义自己的环境变量和别名,当然,<strong>oh my zsh</strong> 在安装时已经自动读取当前的环境变量并进行了设置,你可以继续追加其他环境变量:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/zsh/environmental-variables.png" alt="image"></p><p>接下来进行别名的设置,部分常用别名设置如下:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/zsh/alias.png" alt="image"></p><p>设置完环境变量和别名之后,<strong>Zsh</strong> 基本上就很好用了。如果你是个主题控,还可以玩玩 <strong>Zsh</strong> 的主题。在 .zshrc 里找到ZSH_THEME,就可以设置主题了,默认主题是:</p><blockquote><p>ZSH_THEME=”robbyrussell”</p></blockquote><p><strong>oh my zsh</strong> 提供了数十种主题,相关文件在 <strong>~/.oh-my-zsh/themes</strong> 目录下,默认提供了100多种,你可以随意选择,主题效果可在[<strong>这里</strong>]预览,也可以编辑主题满足自己的变态需求,我就用的默认主题 <strong>robbyrussell</strong>,大致效果如下:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/zsh/theme-robbyrussell.png" alt="image"></p><h2 id="插件"><a href="#插件" class="headerlink" title="插件"></a>插件</h2><hr><p>oh my zsh 项目提供了完善的插件体系,相关文件在 <strong>~/.oh-my-zsh/plugins</strong> 目录下,默认提供了200多种,大家可以根据自己的实际学习和工作环境采用,各插件功能可在<a href="https://github.com/robbyrussell/oh-my-zsh/wiki/Plugins-Overview" target="_blank" rel="noopener"><strong>这里</strong></a>进行简单了解,而具体细节则需打开相关目录下的 <strong>.zsh</strong> 文件查看。插件也是在 <strong>.zshrc</strong> 里配置,找到 <strong>plugins</strong> 关键字,你就可以加载自己的插件了,系统默认加载 <strong>git</strong> ,你可以在后面追加内容,我暂时启用了以下4个插件:</p><blockquote><p>plugins=(git autojump z extract)</p></blockquote><p>上述插件功能说明如下:<br>(1)<strong>git</strong><br> 当你处于一个 git 受控的目录下时,Shell 会明确显示 git 和 branch,如下图所示,另外对 git 很多命令进行了简化:</p><style> table th:nth-of-type(1){ width:150px; }</style><style> table th:nth-of-type(2){ width:150px; }</style><table><thead><tr><th style="text-align:left">Alias</th><th style="text-align:left">Command</th><th style="text-align:left">Function</th></tr></thead><tbody><tr><td style="text-align:left">g</td><td style="text-align:left">git</td><td style="text-align:left">命令原型</td></tr><tr><td style="text-align:left">ga</td><td style="text-align:left">git add</td><td style="text-align:left">将本地修改保存至暂存区以备提交</td></tr><tr><td style="text-align:left">gb</td><td style="text-align:left">git branch</td><td style="text-align:left">查看本地分支</td></tr><tr><td style="text-align:left">gcam</td><td style="text-align:left">git commit -a -m</td><td style="text-align:left">提交暂存区修改,后接自定义提交日志信息</td></tr><tr><td style="text-align:left">gd</td><td style="text-align:left">git diff</td><td style="text-align:left">查看当前分支相比上次提交的版本的修改情况</td></tr><tr><td style="text-align:left">gl</td><td style="text-align:left">git pull</td><td style="text-align:left">从远端仓库拉取更新</td></tr><tr><td style="text-align:left">gp</td><td style="text-align:left">git push</td><td style="text-align:left">将本地分支推送到远端仓库</td></tr><tr><td style="text-align:left">gco</td><td style="text-align:left">git checkout</td><td style="text-align:left">切换当前分支</td></tr><tr><td style="text-align:left">gm</td><td style="text-align:left">git merge</td><td style="text-align:left">合并远端分支与本地分支</td></tr><tr><td style="text-align:left">gst</td><td style="text-align:left">git status</td><td style="text-align:left">查看本地分支情况(未暂存、已暂存未提交)</td></tr></tbody></table><p>熟练使用可以大大减少 git 的命令长度,更多内容可以参考本地文件:</p><blockquote><p><strong>~/.oh-my-zsh/plugins/git/git.plugin.zsh</strong></p></blockquote><p>(2)<strong>extract</strong><br> 一个功能强大的解压插件,所有类型的文件解压只用一个命令 <strong>x</strong> 全搞定,再也不需要去记tar后面到底是哪几个参数了(上面的.zshrc中的别名配置中有提到不同类型的归档文件的解压命令和参数各不相同)。</p><p>(3)<strong>autojump</strong>与<strong>z</strong>:,<br>经常在命令行下工作的人应该都会遇到过这种情况,需要在几个目录直接来回跳转,不停的 <strong>cd</strong>,效率很低,有的时候还容易进入错的目录。虽然有 <strong>pushd</strong> 和 <strong>popd</strong> 这样的命令存在,但是还是不能做到随心所欲的跳转,于是一些目录跳转工具就被发明出来了。</p><p>它们的基本原理是,在每次 <strong>cd</strong> 的时候记录当前的路径,将这些路径按照 <strong>cd</strong> 进入的次数排序,也就是学习你经常使用的目录。一段时间之后,基本上可以通过前几个字母就能区分出你想进入的目录了,然后就可以输入前几个字母直接进行跳转,而不需要各种 <strong>cd</strong>。</p><p>这类工具中最早的应该是大名鼎鼎的 <a href="https://linux.cn/article-3401-1.html" target="_blank" rel="noopener"><strong>autojump</strong></a>,GitHub项目地址在<a href="https://github.com/wting/autojump" target="_blank" rel="noopener"><strong>这里</strong></a>,快捷命令是 <strong>j</strong>。<strong>autojump</strong> 使用Python编写,对于Bash和Shell的支持都比较好。但是可能是因为是Python写的吧,有的时候会感觉反应有些慢。详细配置参见这篇<a href="http://macshuo.com/?p=676" target="_blank" rel="noopener"><strong>MacTalk</strong></a>。</p><p>有了 <strong>j</strong> 之后,又有了 <strong>z</strong>。<strong>z</strong> 的介绍就是”更好的 <strong>j</strong>“。它的功能和 <strong>j</strong> 基本是相同的,不过它使用Shell脚本编写,更加简洁,基本不会拖慢终端的响应速度。我比较喜欢简洁的,现在看来Github上大部分人也是, <strong>j</strong> 得到了3000+的star,超越了它的前辈 <strong>autojump</strong>。进一步了解请参见<a href="http://www.zcfy.cc/article/become-a-command-line-power-user-with-oh-my-zsh-and-z" target="_blank" rel="noopener"><strong>这篇</strong></a>,详细用法参见 <strong>z</strong> 的<a href="https://github.com/rupa/z" target="_blank" rel="noopener"><strong>Github项目地址</strong></a>。</p><p>然而人们还不满足,于是又有了大杀器 <a href="https://github.com/clvv/fasd" target="_blank" rel="noopener"><strong>Fasd</strong></a>。<strong>Fasd</strong> 不光会记录目录,还会记录文件,也就是说它可以做到快捷打开某个深层目录的文件。<strong>Fasd</strong> 还可以通过配置,实现更加高级的功能。<strong>Fasd</strong> 与 <strong>Zsh</strong> 的结合也非常好,可以使用Tab键灵活的在几个目录中选择。可能是由于 <strong>Fasd</strong> 太强大了,虽然它使用Shell脚本写的,但是在使用的时候还是会感觉拖慢了终端的速度,特别是在执行 <strong>ls -l</strong> 的时候,会感觉输出明显慢了一拍。</p><p>三个工具各有各的特点,人们在追求命令行工作的效率上真的是永无止境的。如果有新的、更好的工具出现,欢迎留言告诉我,感谢赐教。</p><p>最后说一句,虽然 <strong>oh-my-zsh</strong> 提供了很多插件,不过也不要贪多哦,大量的插件会拖慢打开的速度,只安装常用的就好了。</p><h2 id="上手"><a href="#上手" class="headerlink" title="上手"></a>上手</h2><hr><p><strong>Zsh</strong> 兼容 <strong>Bash</strong>,切换过来毫无压力,该咋用咋用。但相比后者,<strong>Zsh</strong> 还具有以下特色:</p><ol><li><strong>强大的历史纪录功能</strong>:输入 grep 然后用上下箭头可以翻阅你执行的所有 grep 命令;</li><li><strong>智能拼写纠正</strong>:输入gtep mactalk * -R,系统会提示:zsh: correct ‘gtep’ to ‘grep’ [nyae]? 比妹纸贴心吧,她们向来都是让你猜的……</li><li><strong>各种补全</strong>:路径补全、命令补全,命令参数补全,插件内容补全等等。触发补全只需要按一下或两下 tab 键,补全项可以使用 ctrl+n/p/f/b上下左右切换。比如你想杀掉 java 的进程,只需要输入 kill java + tab键,如果只有一个 java 进程,zsh 会自动替换为进程的 pid,如果有多个则会出现选择项供你选择。ssh + 空格 + 两个tab键,zsh会列出所有访问过的主机和用户名进行补全;</li><li><strong>智能跳转</strong>:安装了autojump之后,zsh 会自动记录你访问过的目录,通过 j + 目录名 可以直接进行目录跳转,而且目录名支持模糊匹配和自动补全,例如你访问过hadoop-1.0.0目录,输入j hado 即可正确跳转。j –stat 可以看你的历史路径库;5. <strong>目录浏览和跳转</strong>:输入 d,即可列出你在这个会话里访问的目录列表,输入列表前的序号,即可直接跳转;</li><li><strong>目录名直接跳转</strong>:在当前目录下输入 .. 或 … ,或直接输入当前目录名都可以跳转,你甚至不再需要输入 cd 命令了;</li><li><strong>通配符搜索</strong>:ls -l <em>*/</em>.sh,可以递归显示当前目录下的 shell 文件,文件少时可以代替 find,文件太多就了;</li><li><strong>更强的别名</strong>:请参考 <strong>配置</strong> 一节;</li><li><strong>插件支持</strong>:请参考 <strong>插件</strong> 一节。</li></ol><p>本小节内容写作时参考了多方资料,尤其是<a href="http://macshuo.com/?p=676" target="_blank" rel="noopener">[1]</a>,特此感谢!</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><hr><p>[1]<a href="http://macshuo.com/?p=676" target="_blank" rel="noopener">终极Shell-MacTalk</a><br>[2]<a href="https://skyline75489.github.io/post/2014-12-13_j-z-and-fasd.html" target="_blank" rel="noopener">j,z和fasd——Shell目录跳转工具</a><br>[3]<a href="http://notes.11ten.net/zsh.html" target="_blank" rel="noopener">Ubuntu下安装 Oh-My-Zsh 和 autojump</a><br>[4]<a href="http://www.zcfy.cc/article/become-a-command-line-power-user-with-oh-my-zsh-and-z" target="_blank" rel="noopener">通过 Oh-My-Zsh 和 Z 成为一个命令行高手</a></p>]]></content>
<categories>
<category> Operating System </category>
</categories>
<tags>
<tag> Linux </tag>
<tag> Zsh </tag>
<tag> MacOS </tag>
<tag> Shell </tag>
</tags>
</entry>
<entry>
<title>Ubuntu 16.04 下一键配置编程开发环境</title>
<link href="/2018/01/16/OS/Installation/Ubuntu/runtime/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><p>本文详细介绍了 Ubuntu 16.04 系统安装之后的开发环境搭建过程,包括Shell(zsh/fish)、Git、Vim等必备工具,C/C++、Java与Python等编程语言SDK的安装与配置方法。<br><a id="more"></a></p><h2 id="安装方式"><a href="#安装方式" class="headerlink" title="安装方式"></a>安装方式</h2><hr><p>Ubuntu系统中,安装软件通常有如下三种方式:<strong>apt-get</strong>、<strong>dpkg</strong>、<strong>源码编译安装</strong>。</p><h3 id="apt-get"><a href="#apt-get" class="headerlink" title="apt-get"></a>apt-get</h3><p>Ubuntu 软件源本质上是一个软件仓库,我们可以通过以下命令来从仓库中下载并安装软件:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo apt-get install 软件包名 </span><br></pre></td></tr></table></figure></p><p>上面命令中提到的 <strong>apt-get</strong> 是 Ubuntu 系统中的一个包管理工具,通过该方式安装新软件的优缺点小结如下:</p><blockquote><p><strong>优点</strong>:官方源或PPA下载安装的软件版本经过多次测试验证,与本系统兼容性最好,且安装方便,一条命令就能搞定;<br><strong>缺点</strong>:由于加入官方仓库的审核测试流程复杂而漫长,导致仓库中的软件版本通常都不是最新的,甚至落后好几个版本,无法使用其最新特性,在安装配置有特殊软件版本依赖的软件时,可能会出现版本不够的问题。</p></blockquote><p><strong>apt-get</strong> 安装文件的一些<strong>默认路径</strong>:</p><blockquote><p><strong>/var/cache/apt/archives</strong>:下载的软件存放位置;<br><strong>/usr/share</strong>:安装后软件默认位置;<br><strong>/usr/bin</strong>:可执行文件位置;<br><strong>/usr/lib</strong>:库文件位置;<br><strong>/etc</strong>:配置文件位置。</p></blockquote><p>现在你知道为啥 <strong>apt-get install</strong> 前面总要加 <strong>sudo</strong> 了 —— 安装过程会要求足够的权限以访问上述路径。</p><p><strong>apt-get</strong> 其它几个常用命令如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"># 卸载软件</span><br><span class="line"># ① 只卸载软件,不删除配置文件等软件相关文件:</span><br><span class="line">$ sudo apt-get remove 软件包名</span><br><span class="line"># ② 完全卸载软件及包括配置文件在内的所有相关文件:</span><br><span class="line">$ sudo apt-get purge 软件包名</span><br><span class="line"># 或</span><br><span class="line">$ sudo apt-get remove 软件包名 --purge</span><br><span class="line"># ③ 卸载软件并卸载相关依赖:</span><br><span class="line">$ sudo apt-get autoremove 软件包名</span><br><span class="line"># 升级已安装软件</span><br><span class="line">$ sudo apt-get upgrade 软件包名</span><br></pre></td></tr></table></figure><p>apt-get是从软件仓库中获取并安装软件源的工具,<a href="http://dongdongdong.me/2018/01/15/OS/Reinstallation/ubuntu/"><strong>前文</strong></a>提到过,Ubuntu会从以下两处加载配置:</p><blockquote><p><strong>/etc/apt/source.list</strong><br><strong>/etc/apt/source.list.d</strong></p></blockquote><p>前者是<strong>官方软件仓库源</strong>列表,包含绝大多数Ubuntu系统软件,默认是采用的<a href="http://cn.archive.ubuntu.com/ubuntu/" target="_blank" rel="noopener"><strong>官方中国镜像源</strong></a>,教育网推荐<a href="https://mirrors.tuna.tsinghua.edu.cn/ubuntu/" target="_blank" rel="noopener"><strong>清华镜像源</strong></a>。此前,Ubuntu 还没有位于国内的镜像源,从 Ubuntu 官方源下载软件比较慢,所以通常需要更换软件源来加快下载速度。互联网上有很多开源镜像站点,具体见<a href="http://wiki.ubuntu.org.cn/%E6%A8%A1%E6%9D%BF:16.04source" target="_blank" rel="noopener"><strong>此处</strong></a>。选择源列表的时候,可以先使用 ping 命令测试一下网络速度,选择最快的源。</p><p>后者是一个目录,存放的是记录各PPA源镜像地址的配置文件,执行以下命令可以修改PPA源:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"># 新增PPA源</span><br><span class="line">$ sudo add-apt-repository ppa:用户名/软件包名</span><br><span class="line"># 删除PPA源</span><br><span class="line">$ sudo add-apt-repository -r ppa:用户名/软件包名</span><br></pre></td></tr></table></figure></p><p>PPA软件源即 <strong>Personal Package Archives (个人软件包档案)</strong>。有些软件没有被选入 UBuntu 官方软件仓库,但为了方便Ubuntu用户使用,<a href="https://launchpad.net/" target="_blank" rel="noopener"><strong>Launchpad</strong></a> 提供了PPA,允许用户建立自己的软件仓库,自由的上传软件。PPA也被用来对一些打算进入Ubuntu官方仓库的软件,或者某些软件的新版本进行测试。</p><p>Launchpad 是 Ubuntu 母公司 Canonical 有限公司所架设的网站,是一个提供维护、支援或联络 Ubuntu 开发者的平台。</p><p>无论是修改了官方源还是PPA源,都需要执行以下命令以更新软件源列表,使修改生效:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo apt-get update</span><br></pre></td></tr></table></figure></p><h3 id="dpkg"><a href="#dpkg" class="headerlink" title="dpkg"></a>dpkg</h3><p><strong>deb</strong> 是 <strong>debian</strong> 软件包安装格式,跟 <strong>red hat</strong> 的 <strong>rpm</strong> 非常相似,通常可以到软件产品的提供商的官方网站上找到 <strong>deb</strong> 安装包,最基本的安装命令是:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ dpkg -i 软件包名.deb</span><br></pre></td></tr></table></figure></p><p>此时若报缺乏特定依赖的错误,可以使用如下命令一键安装依赖:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo apt-get -f install</span><br></pre></td></tr></table></figure></p><p><strong>dpkg</strong> 是 <strong>Debian Package</strong> 的简写,是为 <strong>Debian</strong> 专门开发的套件管理系统,方便软件的安装、更新及移除。所有源自 <strong>Debian</strong> 的 <strong>Linux</strong> 发行版都使用 <strong>dpkg</strong>,例如 <strong>Ubuntu</strong>、<strong>Knoppix</strong> 等等。 </p><p>通过该方式安装新软件的优缺点小结如下:</p><blockquote><p><strong>优点</strong>:在官方下载的指定系统的安装包后,版本最新,安装方便,一条命令就能搞定;<br><strong>缺点</strong>:经常会因缺乏依赖导致安装出错,但好在也只需一条命令即可安装全部所需依赖。</p></blockquote><p>Ubuntu 中所有 <strong>packages</strong> 的信息都在 <strong>/var/lib/dpkg/</strong>目录下,其中子目录 <strong>/var/lib/dpkg/info</strong> 用于保存各个软件包的配置文件列表.不同后缀名代表不同类型的文件,如:</p><blockquote><p><strong>.conffiles</strong>:记录软件包的配置文件列表<br><strong>.list</strong>:保存软件包中的文件列表,用户可以从.list的信息中找到软件包中文件的具体安装位置<br><strong>.md5sums</strong>:记录软件包的md5信息,用于包验证<br><strong>.prerm</strong>:脚本在Debian升级包之前运行,主要作用是停止作用于即将升级的软件包的服务,直到软件包安装或升级完成<br><strong>.postinst</strong>:脚本完成Debian包解开之后的配置工作,通常用于执行所安装软件包的相关命令和服务的重新启动<br><strong>/var/lib/dpkg/available</strong>:文件的内容是软件包的描述信息,该软件包括当前系统所使用的Debian安装源中的所有软件包,其中包括当前系统中已安装的和未安装的软件包</p></blockquote><p>以下是一些 <strong>dpkg</strong> 的常见用法:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">$ dpkg -i 软件包名.deb # 安装一个 Debian 软件包,如你手动下载的文件</span><br><span class="line">$ dpkg -r 软件包名 # 移除一个已安装的包</span><br><span class="line">$ dpkg -c 软件包名.deb # 列出 package.deb 的内容</span><br><span class="line">$ dpkg -I 软件包名.deb # 从 package.deb 中提取包信息</span><br><span class="line">$ dpkg -P 软件包名 # 完全清除一个已安装的包,remove 仅删除数据和可执行文件,purge 另还删除所有相关的配置文件</span><br><span class="line">$ dpkg -L 软件包名 # 列出包安装的所有文件清单</span><br><span class="line">$ dpkg -s 软件包名 # 显示已安装的包的信息</span><br><span class="line">$ dpkg-reconfigure 软件包名 # 重新配置一个已经安装的包</span><br><span class="line">$ dpkg -S 软件包名 # 查看软件在哪个包里</span><br></pre></td></tr></table></figure><h3 id="源码编译"><a href="#源码编译" class="headerlink" title="源码编译"></a>源码编译</h3><p>源码来源:</p><blockquote><p>官方源码压缩包<br>从GitHub等软件仓库中拉取</p></blockquote><p>通过此方式安装软件的优缺点小结如下:</p><blockquote><p><strong>优点</strong>:版本无疑最新,甚至还可以安装尚未正式释出的测试版,另还可灵活指定安装路径;<br><strong>缺点</strong>:编译、配置步骤较为繁琐,经常需要查看错误日志定位问题。</p></blockquote><p><strong>编译安装的文件一般安装在/usr/local/filename</strong><br>具体命令如下:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">$ cd 源码文件夹</span><br><span class="line">$ ./autogen.sh # 生成configure脚本</span><br><span class="line">$ ./configure --prefix=/usr/local/软件名</span><br><span class="line">$ make -j4 # 多(四)线程编译</span><br><span class="line">$ sudo make install # 安装</span><br></pre></td></tr></table></figure></p><p>关于此步骤,我在之前一篇博文中有写到:</p><blockquote><p>以上命令会将此软件相关的文件都会”安装“到同一个文件夹<strong>/usr/local/软件名</strong>下去,这样做的一个显而易见的好处是:如果之后想要删除该文件,或安装更新版本时要求彻底卸载旧版本,只需删除此文件夹即可实现完全删除,再也不用担心由于卸载不干净导致影响新版本安装配置失败的问题。<br>然而这样做在方便软件管理的同时也带来了一个额外的操作:每次安装新命令时,需要配置<strong>PATH</strong>环境变量,将该命令的安装文件夹下的<strong>bin</strong>文件夹的绝对路径添加到原有<strong>PATH</strong>变量后方才能使用新安装软件命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ vim ~/.zshrc # 本人习惯将环境变量定义在此配置文件中 </span><br><span class="line"># 末尾增加一行 export PATH=$PATH:/usr/local/软件名/bin</span><br><span class="line">$ source ~/.zshrc # 令新修改的配置文件生效</span><br></pre></td></tr></table></figure></p></blockquote><p>作为开发人员,大多时候还是更倾向于编译安装,后文中我会根据软件实际选择最合适的安装方式,让我们开始吧~</p><h2 id="必备工具"><a href="#必备工具" class="headerlink" title="必备工具"></a>必备工具</h2><hr><h3 id="Git"><a href="#Git" class="headerlink" title="Git"></a>Git</h3><p>最方便的方式,当然是 <strong>Apt</strong> 安装:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo apt-get install git</span><br></pre></td></tr></table></figure></p><p>缺点之前也说了,版本太老,截至到本文完成,在 Ubuntu 16.04 系统上通过该方式安装的 <strong>Git</strong> 版本才 <strong>2.7.4</strong>,而官方已经释出的版本到了 <strong>2.16.1</strong>,不多说,源码安装步骤如下:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">$ wget https://www.kernel.org/pub/software/scm/git/git-2.16.1.tar.gz # 拉取官方源码包</span><br><span class="line">$ tar zxvf git-2.16.1.tar.gz</span><br><span class="line">$ cd git-2.16.1</span><br><span class="line">$ ./configure --prefix=/usr/local/git # 配置Git安装路径</span><br><span class="line">$ make -j4 # 4线程编译</span><br><span class="line">$ sudo make install # 安装Git到/usr/local/git中</span><br><span class="line">$ git --version # 查看已安装版本</span><br></pre></td></tr></table></figure></p><h3 id="Zsh"><a href="#Zsh" class="headerlink" title="Zsh"></a>Zsh</h3><p>参见<a href="http://dongdongdong.me/2018/01/17/OS/Installation/Ubuntu/zsh/"><strong>这篇</strong></a></p><h3 id="Vim"><a href="#Vim" class="headerlink" title="Vim"></a>Vim</h3><p>参见<a href="http://dongdongdong.me/2018/01/22/OS/Installation/Ubuntu/vim/"><strong>这篇</strong></a></p><h2 id="编程语言"><a href="#编程语言" class="headerlink" title="编程语言"></a>编程语言</h2><hr><h3 id="C-C"><a href="#C-C" class="headerlink" title="C/C++"></a>C/C++</h3><p>在Ubuntu下搭建C/C++编程环境,综合起来就是:<strong>Vim + GCC + GDB</strong>,其中 <strong>Vim</strong> 用于文本编辑,<strong>GCC</strong> 用于程序编译,<strong>GDB</strong> 用于代码调试。</p><p>其实刚装好的系统中已经有 <strong>GCC</strong> 了,但是这个 <strong>GCC</strong> 还无法编译文件,所以要安装 <strong>build-essential</strong>,作用是提供编译程序必须软件包的列表信息,也就是说编译程序有了这个软件包它才知道头文件在哪、库函数在哪,还会下载依赖的软件包,最后才组成一个开发环境。</p><p>安装编程环境,在终端中执行:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo apt-get install build-essential</span><br></pre></td></tr></table></figure></p><h4 id="GCC"><a href="#GCC" class="headerlink" title="GCC"></a>GCC</h4><p><strong>GCC (GNU Compiler Collection)</strong>是一组编译工具的总称,支持多平台、多语言源文件到可执行文件的编译与生成。其中也包括 <strong>gcc(C编译器)</strong> 和 <strong>g++(C++编译器)</strong>。</p><p>在 <strong>GCC</strong> 内部寻找帮助,使用 <strong>gcc –help</strong>,如果想看 <strong>gcc</strong> 选项的完整列表使用 <strong>gcc -v –help 2>&1 | more</strong>。</p><p><strong>GCC</strong> 基本语法格式如下:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"># 对于.c文件</span><br><span class="line">$ gcc [options] [filenames]</span><br><span class="line"># 对于.cpp或.cc</span><br><span class="line">$ g++ [options] [filenames]</span><br></pre></td></tr></table></figure></p><p>命令选项参数及说明如下表:</p><style> table th:nth-of-type(1){ width:100px; }</style><table><thead><tr><th style="text-align:left">Option</th><th style="text-align:left">Description</th></tr></thead><tbody><tr><td style="text-align:left">-c</td><td style="text-align:left">只对文件进行编译和汇编,但不进行连接,生成目标文件”.o”</td></tr><tr><td style="text-align:left">-S</td><td style="text-align:left">只对文件进行编译,但不汇编和连接</td></tr><tr><td style="text-align:left">-E</td><td style="text-align:left">只对文件进行预处理,但不编译汇编和连接</td></tr><tr><td style="text-align:left">-g</td><td style="text-align:left">在可执行程序中包含标准调试信息</td></tr><tr><td style="text-align:left">-o file1 [file2]</td><td style="text-align:left">将文件file1编译成可执行文件file2</td></tr><tr><td style="text-align:left">-v</td><td style="text-align:left">打印出编译器内部编译各过程的命令行信息和编译器的版本</td></tr><tr><td style="text-align:left">-I dir</td><td style="text-align:left">在头文件的搜索路径列表中添加dir目录</td></tr><tr><td style="text-align:left">-L dir</td><td style="text-align:left">在库文件的搜索路径列表中添加dir目录</td></tr><tr><td style="text-align:left">-static</td><td style="text-align:left">强制链接静态库</td></tr><tr><td style="text-align:left">-lNAME</td><td style="text-align:left">连接名为libNAME的库文件</td></tr><tr><td style="text-align:left">-Wall -W</td><td style="text-align:left">开启GCC最常用的警告,GCC的warning一般格式为:file:line-number:message</td></tr><tr><td style="text-align:left">-pedantic</td><td style="text-align:left">要求严格符合ANSI标准</td></tr><tr><td style="text-align:left">-Wconversion</td><td style="text-align:left">开启隐式类型转换警告</td></tr><tr><td style="text-align:left">-Wshadow</td><td style="text-align:left">开启同名变量函数警告</td></tr><tr><td style="text-align:left">-Wcast-qual</td><td style="text-align:left">开启对特性移除的cast的警告,如const</td></tr><tr><td style="text-align:left">-o(-o1)</td><td style="text-align:left">对编译出的代码进行优化</td></tr><tr><td style="text-align:left">-o2</td><td style="text-align:left">进行比-o高一级的优化</td></tr><tr><td style="text-align:left">-o3</td><td style="text-align:left">产生更高级别的优化</td></tr><tr><td style="text-align:left">-os</td><td style="text-align:left">产生最小的可执行文件</td></tr><tr><td style="text-align:left">-pg</td><td style="text-align:left">开启性能测试,记录每个函数的调用次数与时长</td></tr><tr><td style="text-align:left">-ftest-coverage</td><td style="text-align:left">记录每一行被执行的次数</td></tr><tr><td style="text-align:left">-fprofile-arcs</td><td style="text-align:left">记录每个分支语句执行的频率</td></tr></tbody></table><h4 id="GDB"><a href="#GDB" class="headerlink" title="GDB"></a>GDB</h4><p><strong>GDB</strong> 是一个用来调试 <strong>C/C++</strong> 程序的功能强大的调试器,能在程序运行时观察程序的内部结构和内存使用情况,它主要提供以下功能:</p><ul><li>监视程序中变量的值的变化</li><li>设置断点,使程序在指定的代码行上暂停执行,便于观察</li><li>单步执行代码</li><li>分析崩溃程序产生的core文件</li></ul><p>通过在 <strong>GDB</strong> 下输入 <strong>help</strong> 或在命令行上输入 <strong>gdb h</strong> 查看关于 <strong>gdb</strong> 选项说明的简单列表。键入 <strong>help</strong> 后跟命令的分类名。可以获得该类命令的详细清单。搜索和 <strong>word</strong> 相关的命令可用 <strong>apropos word</strong>。</p><p>为使 <strong>GDB</strong> 能正常工作,必须在程序编译时包含调试信息。即 <strong>-g</strong> 选项。前文有讲解。</p><p>简单的调试步骤示例:</p><ol><li>载入test可执行文件:gdb test –silent</li><li>运行:run/r</li><li>查看程序出错的地方:where</li><li>查看出错函数附近的代码:list/l</li><li>打开堆栈:backtrace/bt</li><li>单步调试:next/n或step</li><li>查看可疑表达式值:print var</li><li>在可疑行打断点:break linenum</li><li>重新运行会在断点处停止。用 set variable 修改变量值</li><li>继续运行:continue,看结果是否正确</li><li>退出gdb:quit</li></ol><p>再放一张 <strong>GDB</strong> 命令示意图:<br>![image](<a href="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/runtime/gdb-commands-list.png" target="_blank" rel="noopener">https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/runtime/gdb-commands-list.png</a> %}</p><h3 id="Java"><a href="#Java" class="headerlink" title="Java"></a>Java</h3><p>高级编程语言都会有自己专门的开发环境,即 <strong>Software Development Kit</strong>,简称 <strong>SDK</strong>,对于 <strong>Java</strong> 而言,最常用的就是 <strong>Oracle</strong> 公司提供的 <strong><a href="http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html" target="_blank" rel="noopener">JDK</a></strong>,全称是 <strong>Java SE Development Kit</strong>,现在官网最新版本是 <strong>jdk-8u161</strong>。</p><p>下载最新版本的 <strong>JDK</strong> 准备之后安装,如果系统预先安装好了 <strong>OpenJDK</strong> ,需要先将其卸载,否则可能出现冲突:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ sudo apt-get purge openjdk/openjdk*</span><br><span class="line">$ sudo apt-get clean/autoclean</span><br></pre></td></tr></table></figure></p><p>解压 <strong>JDK</strong> ,并移动到指定位置:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ tar zxvf jdk-8u161-linux-x64.tar.gz</span><br><span class="line">$ sudo mv ./jdk1.8.0_161 /usr/local/java</span><br></pre></td></tr></table></figure></p><p>配置环境变量:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">$ vim ~/.zshrc</span><br><span class="line"># 将以下内容添加在文件末尾</span><br><span class="line"># export Java_HOME=/usr/local/java/jdk1.8.0_161</span><br><span class="line"># export JRE_HOME=$JAVA_HOME/jre</span><br><span class="line"># export CLASSPATH=.:$CLASSPATH:$JAVA_HOME/lib:$JRE_HOME/lib</span><br><span class="line"># export PATH=$PATH:$JAVA_HOME/bin:$JRE_HOME/bin</span><br><span class="line"># 保存并退出 :wq</span><br><span class="line">$ source ~/.zshrc # 使配置生效</span><br></pre></td></tr></table></figure></p><p>验证安装版本:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ java -version # 显示:java version "1.8.0_161"</span><br><span class="line">$ javac -version # 显示:javac 1.8.0_161 </span><br></pre></td></tr></table></figure></p><h3 id="Python"><a href="#Python" class="headerlink" title="Python"></a>Python</h3><p>Ubuntu系统自带 <strong>Python</strong> 环境,<strong>Python 2</strong> 和 <strong>Python 3</strong> 都有,可以通过以下命令检查版本:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ python -V</span><br><span class="line">$ python3 -V</span><br></pre></td></tr></table></figure></p><p>官网截止到本文发表时的最新 <strong>Python</strong> 版本分别为 <strong>2.7.14</strong> 和 <strong>3.6.4</strong>,下载两个版本 <strong>.xz</strong> 的 <strong>Python</strong> 源码并进行编译安装,步骤类似,以 <strong>Python 2.7.14</strong> 为例:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"># Ubuntu中先要安装ssl相关依赖</span><br><span class="line">$ sudo apt-get install python-dev libffi-dev libssl-dev</span><br><span class="line">$ wget https://www.python.org/ftp/python/2.7.14/Python-2.7.14.tar.xz</span><br><span class="line">$ xz -d Python-2.7.14.tar.xz</span><br><span class="line">$ tar xvf Python-2.7.14.tar</span><br><span class="line">$ cd Python-2.7.14</span><br><span class="line">$ ./configure</span><br><span class="line">$ make -j</span><br><span class="line">$ sudo make install</span><br></pre></td></tr></table></figure></p><p>执行 <strong>configure</strong> 脚本时可以通过 <strong>prefix</strong> 参数指定安装路径,默认会将可执行文件放至 <strong>/usr/local/bin</strong> 下。</p><h4 id="pip"><a href="#pip" class="headerlink" title="pip"></a>pip</h4><p><strong>Python</strong> 语言的优势主要体现在其拥有功能强大的 <strong>库</strong> ,或者说是 <strong>依赖模块</strong>,例如:<strong>Numpy</strong> 适用于科学计算领域,<strong>Pandas</strong> 则带来强大的矩阵计算能力,<strong>Matplotlib</strong>、<strong>Seaborn</strong> 提供了样式繁多的绘图模板使得 <strong>Python</strong> 成为数据可视化领域一个有力工具,HTTP方面也有 <strong>urllib3</strong>、<strong>requests</strong> 等,而 <strong>Flask</strong> 更是一个性能优秀、功能完备的网络框架。</p><p>为加快项目产品迭代进度,不去重复造轮子,<strong>Python</strong> 编程的过程中不可避免地需要使用诸多成熟库,对于这些依赖库的管理(安装、升级、卸载等)一般使用 <strong>pip</strong> 包管理器,基本全部的常用库的稳定版本都可以直接从 <strong>pip</strong><br>的软件源中下载,常用命令如下:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ sudo -H pip install package_name # 安装</span><br><span class="line">$ sudo -H pip upgrade package_name # 升级</span><br><span class="line">$ sudo -H pip uninstall package_name # 卸载</span><br><span class="line">$ sudo -H pip list # 列举已安装的包</span><br></pre></td></tr></table></figure></p><p>源码安装的 <strong>Python 3</strong> 中会将最新版本的 <strong>pip</strong> 一并安装,通过 <strong>pip</strong> 安装的库会被安装至 <strong>/usr/local/lib/python3.6/site-packages</strong>。而 <strong>python 2</strong> 在编译安装时则不会安装 <strong>pip</strong> ,可通过以下方式安装:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ wget https://bootstrap.pypa.io/get-pip.py</span><br><span class="line">$ python2 get-pip.py</span><br></pre></td></tr></table></figure></p><p>和 <strong>Python</strong> 分为 <strong>python2</strong> 和 <strong>python3</strong> 一样,<strong>pip</strong> 同样分为 <strong>pip2</strong> 和 <strong>pip3</strong> ,且分别为当前安装的 <strong>python2</strong> 和 <strong>python3</strong> 服务,即 <strong>pip2</strong> 安装的包只供 <strong>python2</strong> 环境导入使用的,<strong>pip3</strong> 安装的包只供 <strong>python3</strong> 环境导入使用。</p><h2 id="博客框架"><a href="#博客框架" class="headerlink" title="博客框架"></a>博客框架</h2><hr><p>参见<a href="http://dongdongdong.me/2018/01/25/OS/Installation/Ubuntu/blog-github-hexo.md/"><strong>此篇</strong></a></p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><hr><p>[1]<a href="http://blog.csdn.net/lanchunhui/article/details/51446359" target="_blank" rel="noopener">Ubuntu的使用-apt-get与dpkg</a><br>[2]<a href="https://github.com/nodejh/nodejh.github.io/issues/26" target="_blank" rel="noopener">Ubuntu软件源</a><br>[3]<a href="http://www.cnblogs.com/bing-yu12/p/6384447.html" target="_blank" rel="noopener">build-essential的作用</a><br>[4]<a href="http://conglang.github.io/2015/03/17/programming-in-ubuntu/" target="_blank" rel="noopener">在Ubuntu下搭建C/C++编程环境</a><br>[5]<a href="https://www.jianshu.com/p/8712cf20a5c9" target="_blank" rel="noopener">Ubuntu 16.04 安装 JDK</a><br>[6]<a href="http://blog.csdn.net/huanhuanq1209/article/details/72673236" target="_blank" rel="noopener">在Ubuntu中安装Python的几种方法</a><br>[7]<a href="https://stackoverflow.com/questions/46752279/lsb-release-not-working-after-install-python-3-6-3-from-source" target="_blank" rel="noopener">从源码安装 Python3.6.3 后lsb_release失效-Stack Overflow</a><br>[8]<a href="https://www.jianshu.com/p/553f9237576c" target="_blank" rel="noopener">pip 缺少 TLS/SSL Module</a></p>]]></content>
<categories>
<category> Operating System </category>
</categories>
<tags>
<tag> Git </tag>
<tag> Ubuntu </tag>
<tag> Hexo </tag>
<tag> Vim </tag>
<tag> Zsh </tag>
<tag> C/C++ </tag>
<tag> Java </tag>
<tag> Python </tag>
</tags>
</entry>
<entry>
<title>记一次 Ubuntu 16.04 重装前后</title>
<link href="/2018/01/15/OS/Installation/Ubuntu/before-after-reinstallation/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><hr><p>前几天实验室PC网络功能出Bug,有线网连不上nju登录站点,而加载内核驱动模块后的无线网卡却妥妥的没有问题,本来懒癌发作打算用一世无线网…可又想到之后虚拟机中的虚拟网卡NAT上网会有坑,且无线网本身速度不够稳定,考虑再三,拉上<a href="http://www.cnblogs.com/burningTheStar/" target="_blank" rel="noopener"><strong>Quber</strong></a>一起“愉快”地开始了找坑之旅。然而,嗯…虽然折腾的结果并不总是那么令人振奋,但付出总有回报。谨以此文纪念本人各种努(chui)力(si)尝(zheng)试(zha),以及此后愤而重装的心路历程。<br><a id="more"></a></p><h2 id="重装的前夜"><a href="#重装的前夜" class="headerlink" title="重装的前夜"></a>重装的前夜</h2><hr><h3 id="挣扎是徒劳的"><a href="#挣扎是徒劳的" class="headerlink" title="挣扎是徒劳的"></a>挣扎是徒劳的</h3><p>遇事儿的第一反应总是想解决问题,只不过每每碰上超出能力范围的…<br>不!还是要挣扎一下,万一成功了呢!</p><p>以下梳理一下Bug出现的前因后果:</p><blockquote><p>硬件配置:<br>(1)<strong>操作系统</strong>:Ubuntu 16.04<br>(2)<strong>内核版本</strong>:4.13.0-26-generic<br>(3) <strong>有线网卡</strong>:Realtek RTL8111/8168/8411 驱动:r8169<br>(4)<strong>无线网卡</strong>:TPLINK RTL8812 驱动:<a href="https://github.com/gnab/rtl8812au" target="_blank" rel="noopener"><strong>rtl8812</strong></a></p><p>Bug现象:有线网无法连接到<strong>校园网登录门户</strong>,当然就不能上网了。</p><p>可能原因:<br>(1)<strong>硬件故障</strong><br> ① 网线坏了? —毕竟不是一次两次了…<br> ② 网口坏了? —学子莘莘,独坑我一人?<br> ③ 网卡坏了? —可能性比我写程序出Bug大大大多了…<br>(2)<strong>软件故障</strong><br> ① 网卡驱动不匹配? —可为啥之前都是好的…<br> ② 内核中除了网卡驱动之外的网络相关模块故障? —“可为啥之前是好的”二连…我干了啥?<br>(3)<strong>DNS配置错误</strong><br> ① /etc/resolve.conf<br> ② /etc/resolveconf.d/base<br> ③ /etc/resolveconf.d/head</p></blockquote><p>哦对,忘记说了,出事儿之前我正在配置<a href="https://www.sdnlab.com/19912.html" target="_blank" rel="noopener"><strong>P4环境</strong></a>,编译安装了一堆东西,会不会…嗯,下次还是在虚拟机里试试,这个回头再说…</p><p>以上,在我的水平范围内委实只能想到这么多,毕竟…知识总有盲点,宋公常这么安(chao)慰(feng)我们。可很快,它们还是被我一一否决了!</p><p>具体我是这么做的:<br>对于硬件故障,easy,网线坏了换几根好的,网口坏了插几个好的,排除!网卡嘛…暂时咱不折腾了,还得把主机拆开,有点小麻烦,并且可能性微乎其微,讲道理一插上网线就可以<strong>ifconfig</strong>查到已自动分配好校园网IP或者<strong>wireshark</strong>抓到DHCP包的话,其实硬件基本就没毛病了。</p><p>网站无法连接,很大可能是域名解析出了问题,Ubuntu中两个与DNS相关的配置文件出错,测试发现最重要的一个配置文件——<strong>/etc/resolv.conf</strong>,“居然”会根据连接的网络的不同而动态变化:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ cat /etc/resolv.conf</span><br></pre></td></tr></table></figure></p><p>呐,连接到无线网OpenWrt的时候,是这样:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/before-after-reinstallation/resolv-conf-wireless-openwrt.png" alt="image"></p><p>连接到有线网的时候,又变成介样:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/before-after-reinstallation/resolv-conf-wired.png" alt="image"></p><p>网上的建议都是增加新的<strong>nameserver</strong>,比如全国通用DNS地址<strong>114.114.114.114(114.114.115.115备用)</strong>,还有全球通用的Google的DNS服务器IP:<strong>8.8.8.8(8.8.4.4备用)</strong>,OK,每个都试了下,都在这里不管用,可见问题并不出在这里。在后文总结部分再回来讨论这些配置文件。</p><p>至于软件故障,驱动程序首当其冲,google搜索相同系统和网卡,类似不能上网的故障还不少,解决方案也都还完备,一度来了信心,完全把“之前为啥是好的”抛诸脑后,试呗。</p><p>根据<a href="http://www.cnblogs.com/duwanjiang/p/5907634.html" target="_blank" rel="noopener"><strong>经验帖</strong></a>,先确定当前以太网卡型号(注意<strong>grep</strong>命令对大小写敏感,搜关键字<strong>ethernet</strong>你是真的找不到网卡…):<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo lspci | grep Ethernet</span><br></pre></td></tr></table></figure></p><p>网卡型号如下图所示,很普通的以太网卡:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/before-after-reinstallation/ethernet-controller-type.png" alt="image"></p><p>进一步查看网卡所用驱动:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo lshw -C network</span><br></pre></td></tr></table></figure></p><p>注意输出中的driver字段:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/before-after-reinstallation/driver-8169.png" alt="image"></p><p>参考他人说法,更换驱动为r8168极有可能解决问题,于是去到官网找对应内核版本的<a href="http://www.realtek.com.tw/downloads/downloadsView.aspx?Langid=1&PNid=13&PFid=5&Level=5&Conn=4&DownTypeID=3&GetDown=false#2" target="_blank" rel="noopener"><strong>驱动</strong></a>,发现提供Linux内核版本最新至4.7的驱动。<br><a href="/2018/01/15/OS/Installation/Ubuntu/before-after-reinstallation/0010-r8168-8.045.08.tar.bz2" title="r8168驱动下载">r8168驱动下载</a></p><p>解压后,进入驱动源码目录进行安装:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ cd r8168-8.045.08</span><br><span class="line">$ sudo ./autorun.sh</span><br></pre></td></tr></table></figure></p><p>驱动安装完成后重启系统,重复上述命令,或者执行<strong>lsmod</strong>可以发现<strong>r8168</strong>驱动已经生效,然而,联网问题依然存在,也许是我与原博主发生的故障原因根本就不一样,自然不能适用。</p><p>驱动问题应该是可以排除了,痛定思痛,决定在线升级系统,兴许能解决网络模块故障:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ sudo apt-get update</span><br><span class="line">$ sudo apt-get dist-upgrade</span><br><span class="line">$ sudo reboot # 重启完成上述更新的安装</span><br><span class="line">$ sudo update-manager -d # 打开更新管理器</span><br></pre></td></tr></table></figure></p><p>一连串更新操作之后,应该是这个界面:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/before-after-reinstallation/update-manager.png" alt="image"></p><p>然后在某人的怂恿下…冒着软件配置在新系统Ubuntu 18(连官网都没有release…)上面各种不兼容报错的风险,我还是干了,其实这时候已经做好重装的准备了。</p><p>然后你还问然后?死心了!<br>一顿操作猛如虎,殊途同归重装苦/(ㄒoㄒ)/~~。</p><h3 id="辩证地看问题"><a href="#辩证地看问题" class="headerlink" title="辩证地看问题"></a>辩证地看问题</h3><p>重装系统这个事儿吧,熟练了就是很简单的一件事儿,无非就是这么几步:</p><blockquote><p>(1) 找个<strong>系统镜像</strong>,如<a href="https://www.ubuntu.com/download/desktop" target="_blank" rel="noopener"><strong>Ubuntu</strong></a>;<br>(2) 做个<strong>系统启动盘</strong>,推荐<a href="https://cn.ultraiso.net/" target="_blank" rel="noopener"><strong>UltraISO</strong></a>;<br>(3) BIOS设置成从<strong>U盘启动</strong>,<a href="https://jingyan.baidu.com/article/f0e83a25c2e28a22e5910134.html" target="_blank" rel="noopener"><strong>懒人教程</strong></a>送上;<br>(4) U盘启动后根据对应操作系统装机提示按步操作。</p></blockquote><p>以上,操作系统即安装完成,倒不算麻烦,顺利的话20多分钟(Ubuntu)应该能够搞定,日常使用的软件也都提前安装好了或者在应用商店基本都可以找到,基本的工作娱乐需求也基本满足了,但是,有一个问题,装Linux的人会仅仅满足于这些基本的软件功能嘛?——毕竟,以用户友好著称的Windows(仅限于Office…)显然才更应该是大多数人的首选。</p><p>(⊙v⊙)嗯…其实我想说的是,对于追求极致效率、捣鼓不倦的Coder们来说,重装系统的工作明明才刚刚开始好伐!</p><p>既然无可避免,何不带着享(xiang)受(shi)的心情,重新打造自己的虚拟空间?</p><h2 id="重装进行时"><a href="#重装进行时" class="headerlink" title="重装进行时"></a>重装进行时</h2><hr><p>俗话说:“年轻不怕重来,生命在于折腾”,那么,开始吧!<br>(以下系统配置与本人喜好有关,仅供参考)</p><h3 id="目录结构"><a href="#目录结构" class="headerlink" title="目录结构"></a>目录结构</h3><p>PC里面的文件目录将被设计成和浏览器中的书签一样都是精心整理过的,一是强迫症,二是方便找。<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/before-after-reinstallation/chrome-bookmark.png" alt="image"></p><p>主要在用户<strong>home</strong>目录下新建若干常用目录<strong>Mine</strong>、<strong>Code</strong>、<strong>Software</strong>。<br>安装<strong>tree</strong>命令<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo apt-get install tree</span><br></pre></td></tr></table></figure></p><p>查看目录树型结构如下:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/before-after-reinstallation/dir-mine.png" alt="image"><br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/before-after-reinstallation/dir-code.png" alt="image"><br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/before-after-reinstallation/dir-software.png" alt="image"><br><a href="/2018/01/15/OS/Installation/Ubuntu/before-after-reinstallation/dir.sh" title="目录配置脚本下载">目录配置脚本下载</a></p><h3 id="桌面主题"><a href="#桌面主题" class="headerlink" title="桌面主题"></a>桌面主题</h3><p>平时看厌了Ubuntu默认的玫红色桌面和终端,就想换个口味,这里推荐一款Ubuntu的扁平化桌面主题<strong>Flatabulous</strong>,加上扁平化的<strong>icon</strong>,显示效果是这样:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/before-after-reinstallation/theme-flatabulou-desktop-terminal.png" alt="image"></p><p>按下win键触发搜索,界面是这样的:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/before-after-reinstallation/theme-flatabulous-search.png" alt="image"></p><p>怎么样,喜不喜欢?只需三步你值得拥有:</p><p>第一步,安装<strong>Unity Tweak Tool</strong>:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo apt-get install unity-tweak-tool</span><br></pre></td></tr></table></figure></p><p>win键搜索后<strong>Unity Tweak Tool</strong>打开后是这样,其中<strong>theme</strong>和<strong>icon</strong>是在第三步要配置的:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/before-after-reinstallation/theme-unity-tweak-tool.png" alt="image"></p><p>第二步,配置PPA源安装<strong>Flatabulous</strong>主题和i<strong>Flat Icon</strong>图标:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"># theme</span><br><span class="line">$ sudo add-apt-repository ppa:noobslab/themes</span><br><span class="line">$ sudo apt-get update</span><br><span class="line">$ sudo apt-get install flatabulous-theme</span><br><span class="line"># icon</span><br><span class="line">$ sudo add-apt-repository ppa:noobslab/icons</span><br><span class="line">$ sudo apt-get update</span><br><span class="line">$ sudo apt-get install ultra-flat-icons</span><br></pre></td></tr></table></figure></p><p>第三步,使用<strong>Unity Tweak Tool</strong>配置主题和图标:<br>打开<strong>Unity Tweak Tool</strong>,设置<strong>theme</strong>为<strong>Flatabulou</strong>,<strong>icon</strong>为<strong>Ultra Flat</strong>即可。</p><h3 id="输入法"><a href="#输入法" class="headerlink" title="输入法"></a>输入法</h3><p>Ubuntu上面的中文输入法不是很好用,可以安装<a href="https://pinyin.sogou.com/linux/?r=pinyin" target="_blank" rel="noopener"><strong>搜狗输入法</strong></a>进行替代,配置如下:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">$ sudo add-apt-repository ppa:fcitx-team/nightly</span><br><span class="line">$ sudo apt-get update</span><br><span class="line">$ sudo apt-get -y install fcitx</span><br><span class="line">$ sudo apt-get -y install fcitx-config-gtk</span><br><span class="line">$ sudo apt-get -y install fcitx-table-all</span><br><span class="line">$ sudo apt-get -y install im-switch</span><br></pre></td></tr></table></figure></p><p>检查<strong>fcitx</strong>是否安装完成:win键搜索fcitx,再安装搜狗官网下载linux版本的deb包:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo dpkg -i 文件名.deb</span><br></pre></td></tr></table></figure></p><p>在系统设置里面点击语言支持,将输入法从<strong>ibus</strong>改为<strong>fcitx</strong>:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/before-after-reinstallation/sougou-ibus-to-fcitx.png" alt="image"></p><p>注销一次,找到fcitx的配置,添加<strong>sougou pinyin</strong>并设为第一输入法即可:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/before-after-reinstallation/sougou-fcitx-add-sougou.png" alt="image"></p><p>配置完成即可使用,有细心的童鞋还会可能发现一个问题,那就是桌面右上角的任务栏会有两个输入法的图标,一个是搜狗的,另一个是fcitx,并且在打字时还会发现桌面同时出现两个输入框,觉得碍眼的朋友,请这么做,终端执行:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ ps aux | grep fcitx</span><br></pre></td></tr></table></figure></p><p>在输入中找到<strong>fcitx-qimpanel</strong>那一行,然后将本行的第二个字段(pid,即进程号)对应数字记下,杀掉该进程即可:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ kill -9 进程号</span><br></pre></td></tr></table></figure></p><p>好了,多余的输入框和任务栏图标都没啦~</p><h3 id="运行环境"><a href="#运行环境" class="headerlink" title="运行环境"></a>运行环境</h3><p>下面是各种开发环境的配置,包括:</p><ol><li><strong>git</strong></li><li><strong>zsh & oh-my-zsh</strong>(fish也蛮不错的)</li><li><strong>vim & vimplus</strong></li><li><strong>gcc & g++</strong></li><li><strong>jdk</strong></li><li><strong>python2 & python3 & pip</strong></li><li><strong>nodejs & hexo</strong></li><li><strong>mysql & workbench</strong></li><li><strong>ssh</strong></li><li><strong>wireshark</strong></li><li><strong>Unix Network Programming</strong></li></ol><p>配置详见<a href="http://localhost:4000/2018/01/16/OS/Reinstallation/ubuntu-config-runtime/" target="_blank" rel="noopener"><strong>此篇</strong></a></p><h3 id="工作利器"><a href="#工作利器" class="headerlink" title="工作利器"></a>工作利器</h3><p>顺便再安利几款Ubuntu下好用的利器:</p><ol><li><p><strong>IDE</strong><br><strong>JetBrains</strong>家的几个亲儿子就不用多说啦,选择合适量级的新版本装好:<br>(1)<a href="https://www.jetbrains.com/idea/" target="_blank" rel="noopener"><strong>Java - Intellij</strong></a><br>(2)<a href="https://www.jetbrains.com/clion/" target="_blank" rel="noopener"><strong>C/C++ - Clion</strong></a>(vim大神求放过…个人觉得用IDE来管理工程模块代码还是会方便一些)<br>(3)<a href="https://www.jetbrains.com/pycharm/" target="_blank" rel="noopener"><strong>Python - PyCharm</strong></a><br>(4)<a href="https://www.jetbrains.com/webstorm/" target="_blank" rel="noopener"><strong>Java Script - WebStorm</strong></a><br>下载后解压,进入<strong>bin</strong>目录执行<strong>x.sh</strong>(x为IDE名称)的脚本即可打开,回头固定到<strong>Launcher</strong>上就好。<br>觉得哪用得上这么多的看过来,<strong>Microsoft</strong>家的<a href="https://code.visualstudio.com/" target="_blank" rel="noopener"><strong>VSCode</strong></a>绝对能满足你,丰富的插件支持多种常见语言开发:C/C++、Java、Python等,文本编辑则有<strong>MarkDown</strong>和<strong>LaTex</strong>,配置简单,保证够用!<br>还嫌(re)弃(ai)麻(zhe)烦(teng)的,您出门右转那儿有个<a href="https://www.sublimetext.com/" target="_blank" rel="noopener"><strong>Sublimetext</strong></a>,我就不送了…<br>Markdown编辑器的话,比较喜欢<a href="https://remarkableapp.github.io/linux.html" target="_blank" rel="noopener"><strong>Remarkable</strong></a>的风格:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/before-after-reinstallation/ide-markdown-remarkable.png" alt="image"><br>伸手党据此配置同一效果:<strong>View -> Night Mode,Style -> Screen</strong></p></li><li><p><strong>虚拟化</strong><br>(1)<a href="https://www.virtualbox.org/wiki/Linux_Downloads" target="_blank" rel="noopener"><strong>Virtualbox</strong></a><br>(2)<a href="https://my.vmware.com/cn/group/vmware/info/slug/desktop_end_user_computing/vmware_workstation_pro/14_0" target="_blank" rel="noopener"><strong>VMware</strong></a><br>下载官方.deb包使用命令<strong>sudo dpkg -i 文件名.deb</strong>安装即可,如有因缺乏依赖导致安装失败时,根据命令行提示,执行:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo apt-get -f install </span><br></pre></td></tr></table></figure><p>一键安装之前缺乏的依赖,再次安装.deb包即可成功。</p></li><li><p><strong>阅读器</strong><br>推荐<a href="https://apps.ubuntu.com/cat/applications/quantal/okular/" target="_blank" rel="noopener"><strong>Okular</strong></a>,系统自带应用商店<strong>Ubuntu Software</strong>就有:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/before-after-reinstallation/reader-okular.png" alt="image"><br>快捷键F6进入文档操作模式,再按数字键4,即可高亮文档,但是有个坑就是它高亮部分的文件状态信息大概是维护在软件内部的,也就是说根本不会对源文件做修改…这导致换个程序换个机器打开文档后都只能看到未经处理的源文档…</p></li><li><p><strong>截图</strong><br>推荐<strong>Shutter</strong>,安装命令如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo apt-get install shutter</span><br></pre></td></tr></table></figure><p>为其配置系统快捷键<strong>Ctrl+Alt+A</strong>为调用<strong>shutter</strong>进行区域截图,快捷键Name随意起,Command填入<strong>shutter -s</strong>:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/before-after-reinstallation/screenshot-shutter.png" alt="image"></p></li><li><p><strong>音乐、翻译</strong><br><a href="https://music.163.com/#/download" target="_blank" rel="noopener"><strong>网易云</strong></a>Ubuntu 16.04版和<a href="http://cidian.youdao.com/index-linux.html" target="_blank" rel="noopener"><strong>有道</strong></a>Deepin版,不多说,官网的.deb直接安装</p></li><li><p><strong>VPN</strong><br>使用<strong>ShadowSocks</strong>在Ubuntu下科学上网,浏览器和终端翻墙都花了不少功夫,详见<a href="http://localhost:4000/2018/01/21/OS/Reinstallation/ubuntu-config-vpn/" target="_blank" rel="noopener"><strong>此篇</strong></a></p></li></ol><h2 id="重装的收获"><a href="#重装的收获" class="headerlink" title="重装的收获"></a>重装的收获</h2><hr><h3 id="遵循基本规定"><a href="#遵循基本规定" class="headerlink" title="遵循基本规定"></a>遵循基本规定</h3><ul><li><strong>编译安装的文件一般安装在/usr/local/filename</strong><br>具体命令如下:<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">$ cd 源码文件夹</span><br><span class="line">$ ./autogen.sh # 生成configure脚本</span><br><span class="line">$ ./configure --prefix=/usr/local/软件名</span><br><span class="line">$ make -j4 # 多(四)线程编译</span><br><span class="line">$ sudo make install # 安装</span><br></pre></td></tr></table></figure>以上命令会将此软件相关的文件都会”安装“到同一个文件夹<strong>/usr/local/软件名</strong>下去,这样做的一个显而易见的好处是:如果之后想要删除该文件,或安装更新版本时要求彻底卸载旧版本,只需删除此文件夹即可实现完全删除,再也不用担心由于卸载不干净导致影响新版本安装配置失败的问题。<br>然而这样做在方便软件管理的同时也带来了一个额外的操作:每次安装新命令时,需要配置<strong>PATH</strong>环境变量,将该命令的安装文件夹下的<strong>bin</strong>文件夹的绝对路径添加到原有<strong>PATH</strong>变量后方才能使用新安装软件命令:<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ vim ~/.zshrc # 本人习惯将环境变量定义在此配置文件中 </span><br><span class="line"># 末尾增加一行 export PATH=$PATH:/usr/local/软件名/bin</span><br><span class="line">$ source ~/.zshrc # 令新修改的配置文件生效</span><br></pre></td></tr></table></figure>再来理解一下软件“<strong>安装</strong>“的概念:其实就是把软件的编译好的<strong>可执行文件</strong>和<strong>依赖库文件</strong>放到一个系统能够发现的合适位置。那么放在什么路径下才能让系统自动找到呢?这就是环境变量的作用了,在Linux乃至Windows系统中,<strong>PATH</strong>无疑是最重要的一个系统环境变量,它是一个由”:“(Linux)或”;“(Windows)分隔的,由多个软件可执行文件所在路径连接所构成的字符串,在未显式指定可执行文件的情况下,在终端中执行命令,都会到由<strong>PATH</strong>变量中包含的文件路径下面去找该命令对应的可执行文件,如果没有,就会报”<strong>Command Not Found</strong>“的错误。</li></ul><h3 id="善用系统命令"><a href="#善用系统命令" class="headerlink" title="善用系统命令"></a>善用系统命令</h3><ol><li><strong>sudo apt-get -f install</strong><br>正确安装之前由于缺乏依赖而失败的安装过程中全部必要依赖,避免要对照着安装错误日志上面的依赖名称一个个安装。</li><li><p><strong>sudo add-apt-repository ppa:x</strong><br>添加PPA软件源,加入-r参数则表示remove即删除某个之前添加的PPA软件源,回车两下后操作生效,对应的PPA源的增删情况可以在两个地方得到验证:<br><strong>(1)系统设置(System Settings) -> 软件更新(Software & Update) -> 其它软件(Other Software)</strong>:列表中会显示/移除对应的PPA源地址,当然也可以在该UI上直接操作;<br><strong>(2)/etc/apt/source.list.d</strong>:目录下会出现/删除对应PPA源的.list和.list.save文件。</p></li><li><p><strong>whereis、local和which</strong><br>(1)<strong>whereis</strong><br><strong>whereis</strong>命令只能用于程序名的搜索,而且只搜索<strong>二进制文件</strong>(参数-b)、<strong>man说明文件</strong>(参数-m)和<strong>源代码文件</strong>(参数-s)。如果省略参数,则返回所有信息。<br>和<strong>find</strong>相比,<strong>whereis</strong>查找的速度非常快,这是因为linux系统会将 系统内的所有文件都记录在一个数据库文件中,当使用<strong>whereis</strong>和下面即将介绍的<strong>locate</strong>时,会从数据库中查找数据,而不是像<strong>find</strong>命令那样,通过遍历硬盘来查找,效率自然会很高。<br>但是该数据库文件并不是实时更新,<strong>默认情况下时一星期更新一次</strong>,因此,我们在用<strong>whereis</strong>和<strong>locate</strong> 查找文件时,有时会找到已经被删除的数据,或者刚刚建立文件,却无法查找到,原因就是因为数据库文件没有被更新。<br>(2)<strong>locate</strong><br><strong>locate</strong>让使用者可以很快速的搜寻档案系统内是否有指定的档案。其方法是先建立一个包括系统内所有档案名称及路径的数据库,之后当寻找时就只需查询这个数据库,而不必实际深入档案系统之中了。在一般的发行版中,数据库的建立都被放在<strong>crontab</strong>中自动执行。<br><strong>locate</strong>命令其实是”<strong>find -name</strong>“的另一种写法,但要比后者快得多,原因在于它不搜索具体目录,而是搜索一个数据库(Ubuntu下是/var/lib/mlocate/mlocate.db),这个数据库中含有本地所有文件信息。Linux系统自动创建这个数据库,并且每天自动更新一次,所以使用<strong>locate</strong>命令查不到最新变动过的文件。为了避免这种情况,可以在使用<strong>locate</strong>之前,先使用<strong>updatedb</strong>命令,手动更新数据库。<br>(3)<strong>which</strong><br><strong>which</strong>命令的作用是,在<strong>PATH</strong>变量指定的路径中,搜索某个系统命令的位置,并且返回第一个搜索结果。也就是说,使用<strong>which</strong>命令,就可以看到某个系统命令是否存在,以及执行的到底是哪一个位置的命令。</p></li><li><p><strong>echo >> 和echo ></strong><br>在<strong>Shell</strong>编程过程很多时候会使用<strong>echo</strong>并输入到日志文件中。写日志的时候有两种情况,一种是一次写入文件空,再写的时候就将之前的内容给覆盖掉,如何实现追加内容呢?<br>(1)<strong>覆盖写入</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ echo "日志内容" > 文件</span><br></pre></td></tr></table></figure><p>(2)<strong>追加写入</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ echo "日志内容" >> 文件</span><br></pre></td></tr></table></figure></li><li><p><strong>lspci和lsmod</strong><br>(1)<strong>lspci</strong><br><strong>lspci</strong>命令用于显示当前主机的所有PCI总线信息,以及所有已连接的PCI设备信息。<br>(2)<strong>lsmod</strong><br><strong>lsmod</strong>命令用于显示已经加载到内核中的模块的状态信息。执行<strong>lsmod</strong>命令后会列出所有已载入系统的模块。<strong>Linux</strong>内核具有模块化的特性,应此在编译内核时,务须把全部的功能都放入内核。可以将这些功能编译成一个个单独的模块,待需要时再分别载入。</p></li><li><p><strong>insmod、rmmod、modprobe、depmod</strong><br>(1)<strong>insmod</strong><br><strong>insmod</strong>命令用于将给定的模块加载到内核中。Linux有许多功能是通过模块的方式,在需要时才载入内核执行。如此可使内核较为精简,进而提高效率,以及保有较大的弹性。这类可载入的模块,通常是<strong>设备驱动程序</strong>。<br>(2)<strong>rmmod</strong><br><strong>rmmod</strong>命令用于从当前运行的内核中移除指定的内核模块。执行<strong>rmmod</strong>可删除不需要的模块。<br>(3)<strong>modprobe</strong><br><strong>modprobe</strong>命令用于智能地向内核中加载模块或者从内核中移除模块。<br><strong>modprobe</strong>可载入指定的个别模块,或是载入一组相依的模块。<strong>modprobe</strong>会根据<strong>depmod</strong>所产生的相依关系,决定要载入哪些模块。若在载入过程中发生错误,在<strong>modprobe</strong>会卸载整组的模块。<br>(4)<strong>depmod</strong><br><strong>depmod</strong>命令可产生模块依赖的映射文件,在构建嵌入式系统时,需要由这个命令来生成相应的文件,由<strong>modprobe</strong>使用。</p></li><li><p><strong>查看操作系统及内核版本</strong><br>(1)系统版本</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ cat /etc/issue</span><br></pre></td></tr></table></figure><p>直接显示Ubuntu 16.04.3 LTS<br>或:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ sudo lsb_release -a</span><br></pre></td></tr></table></figure><p>显示系统版本号(Release):Ubuntu 16.04.3 LTS,及代号(Codename):Xenial<br>(2)内核版本</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ uname -r</span><br></pre></td></tr></table></figure><p>显示当前内核版本:4.13.0-26-generic</p></li><li><p><strong>查看CPU信息</strong><br>(1)按单个逻辑核粒度</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ cat /proc/cpuinfo</span><br></pre></td></tr></table></figure><p>(2)按总核粒度</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ lscpu</span><br></pre></td></tr></table></figure></li></ol><h3 id="认识配置文件"><a href="#认识配置文件" class="headerlink" title="认识配置文件"></a>认识配置文件</h3><ol><li><p><strong>环境变量配置文件:</strong><br>(1)<strong>/etc/profile</strong>:此文件为系统的<strong>每个用户</strong>设置环境信息。当用户登录时,该文件被执行一次,并从 <strong>/etc/profile.d</strong>目录的配置文件中搜集<strong>shell</strong>的设置。一般用于<strong>设置所有用户使用的全局变量</strong>;<br>(2)<strong>~/.bashrc</strong>:只对<strong>单个用户</strong>生效,当登录以及每次打开新的<strong>bash shell</strong>时,该文件被读取;<br>(3)<strong>~/.zshrc</strong>:只对<strong>单个用户</strong>生效,当登录以及每次打开新的<strong>zsh shell</strong>时,该文件被读取。<br>我的环境变量一般都配置在<strong>~/.zshrc</strong>中,保存文件后别忘了用<strong>source</strong>使之立即生效。</p></li><li><p><strong>sudo配置文件:</strong><br><strong>/etc/sudoers</strong>:从源码编译安装<a href="http://mininet.org/" target="_blank" rel="noopener"><strong>mininet</strong></a>时,执行安装脚本:<strong>sudo ./install.sh</strong>,怎么配置也找不到之前安装好的<strong>git</strong>和<strong>vim</strong>,我们都知道”Command Not Found”错误的原因就是没配置好对应命令的环境变量,于是我把上面提到的配置文件都加上了<strong>GIT_HOME</strong>和<strong>VIM_HOME</strong>,即使切换到<strong>root</strong>用户,仍然报错,于是猜想<strong>sudo</strong>命令虽然是让普通用户能够在执行命令时以<strong>root</strong>权限运行,但是仍不是真正的<strong>root</strong>,上述对所有用户都生效的环境变量都对<strong>sudo</strong>不适用,原因应该是<strong>sudo</strong>自己有特殊的环境变量定义。google到的结果证实了我这一想法,在<strong>/etc</strong>目录下有一个<strong>sudo</strong>命令的配置文件<strong>sudoers</strong>:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/before-after-reinstallation/sudoers-secure-path.png" alt="image"><br>以<strong>sudo</strong>执行命令都会从<strong>/etc/sudoers</strong>这个配置文件中变量<strong>secure_path</strong>的值作为<strong>sudo</strong>的<strong>PATH</strong>环境变量,会去这个变量指定的路径下去找命令对应的可执行文件。将<strong>GIT_HOME</strong>和<strong>VIM_HOME</strong>的添加到<strong>secure_path</strong>末尾,命令就能顺利找到执行了。</p></li><li><p><strong>内核驱动文件:</strong><br><strong>/lib/modules/$(uname -r)/kernel/drivers/</strong>:网卡驱动会放在<strong>net</strong>文件夹中,<strong>ethernet</strong>表示以太网卡,<strong>wireless</strong>中放的是无线网卡驱动。</p></li><li><p><strong>DNS配置文件:</strong><br>(1)<strong>/etc/resolv.conf</strong><br>(2)<strong>/etc/resolvconf/resolvconf.d/base</strong><br>(3)<strong>/etc/resolvconf/resolvconf.d/head</strong><br>里面最关键的就是<strong>nameserver</strong>字段,在其<strong>IP</strong>所对应的<strong>DNS Server</strong>处<strong>search</strong>当前想要连接的<strong>域名</strong>,如<strong>nju.edu.cn</strong>或<strong>lan</strong>等,一般来讲在Ubuntu 16.04系统中,每次更换网络连接后,<strong>resolv.conf</strong>都会自动重新加载,<strong>nameserver</strong>一般为<strong>127.0.1.1</strong>,一个很奇怪的<strong>IP</strong>地址,我们知道本机的回环地址<strong>lo</strong>的<strong>IP</strong>为<strong>127.0.0.1</strong>。查找资料后才知道,原来Ubuntu下是有一个本地DNS服务,叫做<strong>dnsmasq</strong>,由NetworkManager控制,通过以下命令能发现此服务进程信息:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ ps aux | grep dnsmasq # 以下命令效果同上</span><br><span class="line">$ ps -ef | grep dnsmasq</span><br></pre></td></tr></table></figure><p>输出如下:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/before-after-reinstallation/dnsmasq-listen-addr.png" alt="image"><br>可以看到它监听的本地地址,<strong>–listen-address=127.0.1.1</strong>(ubuntu12.04及之前的版本是 127.0.0.1), 这个地址是一个本地回环地址,而你真实的dns服务器地址,是被这个服务管理维护着的。(之后我会写篇<a href="http://localhost:4000/2018/01/21/OS/Reinstallation/ubuntu-dns/" target="_blank" rel="noopener"><strong>文章</strong></a>专门讨论<strong>Ubuntu</strong>下的<strong>DNS</strong>配置)<br>即,用户进程想要访问某个<strong>IP</strong>未知、只知道主机名的服务器时,其发起DNS请求的流程如下:</p><blockquote><p>local process -> local dnsmasq -> router -> ISP DNS</p></blockquote></li><li><p><strong>软件源配置文件:</strong><br>(1)<strong>/etc/apt/source.list</strong>:通常放的是<a href="http://cn.archive.ubuntu.com/ubuntu/" target="_blank" rel="noopener"><strong>官方镜像源</strong></a>,但是中间偶然一天遇到了官方源挂掉的情况,考虑到教育网的缘故,我就更换成了<a href="https://mirror.tuna.tsinghua.edu.cn/help/ubuntu/" target="_blank" rel="noopener"><strong>清华的源</strong></a>;<br>(2)<strong>/etc/apt/source.list.d</strong>:通常放的是PPA等第三方镜像源。</p></li></ol><h3 id="掌握实用操作"><a href="#掌握实用操作" class="headerlink" title="掌握实用操作"></a>掌握实用操作</h3><p>好奇之人对新知识总是充满着渴望,这次掌握几个小<strong>Trick</strong>,提(ke)高(xue)效(tou)率(lan)再进一步!<br>(1)<strong>vim中临时提升编辑者权限</strong><br>在Linux上工作的朋友很可能遇到过这样一种情况,当你用<strong>Vim</strong>编辑完一个文件时,运行<strong>:wq</strong>保存退出,突然蹦出一个错误:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/before-after-reinstallation/vim-readonly.png" alt="image"></p><p>这表明文件是只读的,按照提示,加上!强制保存::w!,结果又一个错误出现:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/before-after-reinstallation/vim-cannot-open-file-for-writing.png" alt="image"></p><p>文件明明存在,为何提示无法打开?查看<strong>Vim帮助文档</strong>对<strong>E212</strong>错误的解释如下:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/OS/Installation/Ubuntu/before-after-reinstallation/vim-help-e212.png" alt="image"></p><p>原来是可能没有权限造成的,此时你才想起,这个文件需要<strong>root</strong>权限才能编辑,而当前登陆的只是普通用户,在编辑之前你忘了使用<strong>sudo</strong>来启动<strong>Vim</strong>,所以才保存失败。于是为了防止修改丢失,你只好先把它保存为另外一个临时文件<strong>temp-file-name</strong>,然后退出<strong>Vim</strong>,再运行<strong>sudo mv temp-file-name readonly-file-name</strong>覆盖原文件。<br>但这样操作过于繁琐。而且如果只是想暂存此文件,还需要接着修改,则希望保留<strong>Vim</strong>的工作状态,比如编辑历史,<strong>buffer</strong>状态等等,该怎么办?能不能在不退出<strong>Vim</strong>的情况下获得<strong>root</strong>权限来保存这个文件?<br>答案是肯定的,执行这样一条命令即可(原理参见<a href="http://feihu.me/blog/2014/vim-write-read-only-file/" target="_blank" rel="noopener"><strong>这篇</strong></a>):<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ :w !sudo tee %</span><br></pre></td></tr></table></figure></p><p>(2)待更…</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><hr><p>[1]<a href="(https://shipengliang.com/software-exp/ubuntu-16-04-rtl811181688411-%E4%B8%8D%E8%83%BD%E4%B8%8A%E7%BD%91-%E7%BB%8F%E5%B8%B8%E6%96%AD%E7%BD%91%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95.html">Ubuntu 16.04 RTL8111/8168/8411 不能上网 经常断网解决办法</a><br>[2]<a href="http://topspeedsnail.com/upgrade-to-ubuntu-16_04-LTS/" target="_blank" rel="noopener">Ubuntu系统升级</a><br>[3]<a href="https://www.jianshu.com/p/463f229c0a20" target="_blank" rel="noopener">Ubuntu下超好看的主题Flatabulous</a><br>[4]<a href="http://www.cnblogs.com/burningTheStar/p/6978209.html" target="_blank" rel="noopener">Ubuntux新安装后的软件准备—秋波</a><br>[5]<a href="http://blog.csdn.net/seek_of/article/details/77920639" target="_blank" rel="noopener">dpkg安装deb缺少依赖包的解决方法</a><br>[6]<a href="http://www.cnblogs.com/EasonJim/p/7114679.html" target="_blank" rel="noopener">Ubuntu截图软件shutter</a><br>[7]<a href="http://blog.csdn.net/lu_embedded/article/details/55803500" target="_blank" rel="noopener">在Ubuntu中添加和删除PPA软件源</a><br>[8]<a href="http://www.ruanyifeng.com/blog/2009/10/5_ways_to_search_for_files_using_the_terminal.html" target="_blank" rel="noopener">Linux的五个查找命令-阮一峰</a><br>[9]<a href="http://www.cnblogs.com/peida/" target="_blank" rel="noopener">每天一个Linux命令</a><br>[10]<a href="http://blog.csdn.net/jakemanse/article/details/8043603" target="_blank" rel="noopener">echo写入文件</a><br>[11]<a href="https://www.jianshu.com/p/8f36fe09e39e" target="_blank" rel="noopener">ubuntu dns服务器127.0.1.1的问题</a></p>]]></content>
<categories>
<category> Operating System </category>
</categories>
<tags>
<tag> Linux </tag>
<tag> Ubuntu </tag>
<tag> Reinstallation </tag>
</tags>
</entry>
<entry>
<title>细说子网</title>
<link href="/2017/12/05/Network/Mechanism/subnet/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><hr><p>最近闲来无事,抄起本<strong><a href="https://book.douban.com/subject/26268767/" target="_blank" rel="noopener">《Wireshark网络分析就是这么简单》</a></strong>想了解下数据包粒度(Packet-level)的网络测量及分析方法,书开篇提出一个面试题,与<strong>子网掩码</strong>相关,乍看心觉easy,咱好歹也是计算机专业学生,本科算是很认真学习过计算机网络课,且不说研究多么深入,但至少对于基础概念及重点问题,诸如OSI分层模型、TCP可靠传输机制、子网划分、路由算法甚至CSMA都如数家珍(毕竟好像教科书上科普重点就这么多…),小小的一个<strong>ping</strong>得通还是<strong>ping</strong>不通的问题,还能难倒我?<br><a id="more"></a></p><p>好吧,我承认,确实有困惑到…<br><br>然后一通想加一通做,再加一通找,于是就有了这篇文章…<br><br>大概…<strong>犯错及随之而来的自尊心作祟才是第一生产力</strong>。</p><h2 id="基础概念"><a href="#基础概念" class="headerlink" title="基础概念"></a>基础概念</h2><hr><p>在本文对<strong><a href="https://en.wikipedia.org/wiki/Subnetwork" target="_blank" rel="noopener">子网(Subnetwork)</a></strong>的相关机制进行阐释前,不管你是否有空或者有足够耐心坚持看完全文,在开头,我都觉得很有必要将会涉及到的基本概念先列出如下:</p><ol><li><strong>IP地址</strong>,即网络层地址,以32位的IPv4地址为例,它通常分为<strong>网络地址(Network Address)</strong>和<strong>主机地址(Host Address)</strong>两部分;<br><br></li><li><p><strong>IP地址分类</strong>,按照网络地址位数划分,一般可分作A、B、C、D四类,各类地址细节如下:</p><ul><li><p><strong>A类地址</strong>:用可变的7位(bit)来标识网络号,可变的24位标识主机号,最前面1位固定为“0”,即A类地址第一个字节的十进制数值范围介于1~126之间。注意,A类地址的第一个字节的十进制数值不能是0和127,127开头的IP地址保留给内部回送函数,专门用作诊断,如127.0.0.1是回环地址lo,用于回路测试;而0则表示该地址是本地宿主机,不能(无需)传送。</p><ul><li>A类地址通常为大型网络提供,全世界总共就只有126个A类网络,每个A类网络最多可以连接16777214(2^24 - 2, 主机位全0的IP地址用于标识本网络地址,主机位全1的IP地址用于标识广播地址,因此有两个IP地址不可用,故可表示的主机数量减2)台主机。</li><li>A类地址的<strong>私有地址</strong>范围为:<strong>10.0.0.0 ~ 10.255.255.255</strong>。<br><br></li></ul></li><li><p><strong>B类地址</strong>:用可变的14位来标识网络号,可变的16位标识主机号,最前面2位固定为“10”,即B类地址第一个字节的十进制数值范围介于128~191之间,前两字节合在一起标识网络地址。</p><ul><li>B类地址适用于中等规模的网络,全世界大约有16000个B类网络,每个B类网络最多可以连接65534(2^16 - 2)台主机。</li><li>B类地址的<strong>私有地址</strong>范围为:<strong>172.16.0.0 ~ 172.31.255.255</strong>。<br><br></li></ul></li><li><p><strong>C类地址</strong>:用可变的21位来标识网络号,可变的8位标识主机号,最前面3位固定为”110“,即C类地址第一个字节的十进制数值范围介于192~223之间,前三字节合在一起标识网络地址,最后一个字节用于标识主机地址。</p><ul><li>C类地址适用于校园网等小型网络,每个C类网络最多可以连接254台主机。</li><li>C类地址的<strong>私有地址</strong>范围为:<strong>192.168.0.0 ~ 192.168.255.255</strong>。<br><br></li></ul></li><li><p><strong>D类地址</strong>:最前面4位固定为”1110“,它是一个专门的保留地址,并不指向特定的网络,目前这一类地址被用于多点广播,即多播(Multicast)中,多播地址用来一次性寻址一组主机,标识共享同一特殊协议的一组计算机。<br></p></li><li><p><strong>E类地址</strong>:最前面5位固定为”11110”,为将来使用保留。<br><br></p></li></ul></li><li><p><strong>子网掩码(Subnet Mask)</strong>,也是32位,用于标识一个IP地址的子网地址,计算子网号时,将计算机十进制的IP地址和子网掩码转换为二进制的形式,然后进行二进制按位与计算(全1则得1,不全1则得0),即得到子网地址。</p></li></ol><p>还有一个,你同样需要知道的,由<strong>TCP/IP协议栈</strong>所规定的事实:</p><ul><li>如果属于同一网段/子网,两个地址间的信息交换<strong>不通过或无需网关(如路由器)</strong>。</li><li>如果不属同一网段/子网,也就是子网号不同,两个地址的信息交换必须<strong>通过网关转发</strong>进行。<br><br></li></ul><p>以及决定上述事实的<strong>原理</strong>:</p><ul><li>以太网中,位于同一个网段/子网内的任意设备,可以两两ping通,仅在二层即数据链路层即可完成通信。在已知对方IP地址但不知道MAC地址情况下,可以通过广播ARP请求及获取ARP应答的方式构建新ARP表项,在通信发起方处维护对端IP地址到其MAC地址的映射,接着封装、发送目的MAC地址为对方MAC地址的以太网帧。<br><br></li></ul><p>了解以上内容,“基本”可以回答那道令人困惑的面试题了。</p><h2 id="就从Ping说起"><a href="#就从Ping说起" class="headerlink" title="就从Ping说起"></a>就从Ping说起</h2><hr><p>面试题内容是这样的:</p><p>现有两台服务器A和B,具体配置信息如下表:</p><table><thead><tr><th style="text-align:center">Server ID</th><th style="text-align:center">IP Address</th><th style="text-align:center">Subnet Mask</th><th style="text-align:center">Default Gateway</th><th style="text-align:center">MAC</th></tr></thead><tbody><tr><td style="text-align:center">Server A</td><td style="text-align:center">192.168.26.129</td><td style="text-align:center"><strong>255.255.255.0</strong></td><td style="text-align:center">192.168.26.2</td><td style="text-align:center">08:00:27:28:b9:11</td></tr><tr><td style="text-align:center">Server B</td><td style="text-align:center">192.168.26.3</td><td style="text-align:center"><strong>255.255.255.224</strong></td><td style="text-align:center">192.168.26.2</td><td style="text-align:center">08:00:27:28:b9:22</td></tr></tbody></table><p>默认网关IP:192.168.26.2/24,MAC地址:08:00:27:28:b9:33</p><p>问题描述如下:</p><blockquote><p>B的子网掩码<strong>本来应该配成</strong>255.255.255.0,<strong>但不小心配成</strong>了255.255.255.224,那么A与B还能正常通信嘛?</p><ul><li>如果不行,说明原因。</li><li>如果可以,请简述通信流程。</li></ul></blockquote><p><br><br>书上给出四个<strong>Plausible</strong>的答案,乍一看,真都好有道理:</p><blockquote><ul><li>版本一<ul><li>答案:“A与B不能通信,因为…这都行的话,子网掩码还有什么用???”</li><li>评价:这位的反证法听上去让人无可质疑!</li></ul></li><li>版本二<ul><li>答案:“A与B可以通信,因为它们通过ARP广播获知对方MAC地址。”</li><li>评价:那子网掩码有什么用?上面的反证法刚好可用来反驳这位。</li></ul></li><li>版本三<ul><li>答案:“A与B可以通信,但所有包都要通过默认网关192.168.26.2转发。”</li><li>评价:请问这么复杂的结果你是怎么想到的?</li></ul></li><li>版本四<ul><li>答案:“A与B不能通信,因为ARP不能跨子网。”</li><li>评价:这个答案倒像是过了脑子的。</li></ul></li></ul></blockquote><p><br><br>关于以上问题的事实:真相只有一个。</p><p>而事实有时候就很残酷:上面一个都不对。</p><p>解释说明题可与选择题不一样,后者结论对了找准选项就能拿满分,前者即使给出正确结论,但是荒谬无稽的解释则常常给阅卷人想要倒扣分的冲动…</p><p>按照正经答题的顺序,先告诉你正确答案:A与B可以通信。</p><p>接下来看看这个过程中发生了什么,以<strong>B ping A</strong>为例,至于为什么不是<strong>A ping B</strong>,请认真阅读下文内容,在之后我会解释原因。</p><p>要得出这个答案,我在Virtualbox上拿VM搭了个简单网络拓扑,两个VM分别作为Server A和Server B,第三个VM作为默认网关,并设置成允许转发:</p><blockquote><p>echo 1 > /proc/sys/net/ipv4/ip_forward</p></blockquote><p>得到的抓包结果如下图所示:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/Mechanism/subnet/wireshark-ping-arp.png" alt="image"><br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/Mechanism/subnet/wireshark-ping-icmp.png" alt="image"></p><p>根据以上Wireshark的抓包结果,结合其中数据包出现的先后顺序,可将<strong>B ping A</strong>的通信流程作如下分析:</p><ol><li><p>在B处,计算B的IP地址,即<strong>源IP地址</strong>,与A的IP地址,即<strong>目的IP地址</strong>,分别和B的子网掩码<strong>按位与</strong>的结果,即应用上文介绍的计算网络地址的唯一方法,发现:</p><blockquote><p>B对应的网络地址为192.168.26.0,而A对应的网络地址为192.168.26.128,结果不同。</p></blockquote><p> 因此B会据此判断:</p><blockquote><p>本机与目的主机A不在同一网段。</p></blockquote><p> 而不在同一网段的主机要进行通信,参考前文中关于跨网络通信的机制的内容,了解到B要想与A通信,必须经过<strong><a href="https://en.wikipedia.org/wiki/Default_gateway" target="_blank" rel="noopener">默认网关</a></strong>(肯定与本机B在同一网段)的转发,故主机B会先把ICMP数据包发往默认网关192.168.26.2,我们知道要发送一个数据包,仅知道目的IP地址是不够的,真正在链路上传输时,目的主机的物理地址即MAC地址也是必须要一同封装在以太网帧中的,然而初始时并不知道默认网关的MAC地址。</p><p> ARP协议的作用正在于此:</p><blockquote><p>B向本网段广播(ff:ff:ff:ff:ff:ff)一个ARP Request包,询问:“谁的IP地址是默认网关的地址,请把你的MAC地址回复给我”。</p></blockquote><p> 于是,本次通信的<strong>第一个数据包</strong>出现了!<br><br></p></li><li>默认网关接收到此ARP请求后检查目的IP发现就是自己,于是将自己的MAC封装在ARP Reply数据包中回复给主机B,这是<strong>第二个数据包</strong>。<br><br></li><li>主机B获知默认网关的MAC地址后,发送一个ICMP Request数据包给它,<strong>第三个数据包</strong>被发送,其实这个包本意是想发送给主机A,但中间需要默认网关帮忙中转一下,故此ICMP包的目的MAC地址为默认网关的MAC地址,但是目的IP地址仍为主机A的IP地址。<br><br></li><li>主机B迅速收到一个来自默认网关的ICMP Redirect数据包,即<strong>第四个数据包</strong>,意在告诉主机B:包我会转发的,你放心吧。但是至于包是不是真的被默认网关转发了,还不得而知,除非在默认网关处也抓到这个包。想到这里,不妨换个思路,根据结果接着往下分析。<br><br></li><li>然后我们在主机B上抓到了<strong>第五个数据包</strong>,居然又是个ARP请求包,根据源MAC地址显示,这个ARP请求来自主机A,那么它想干什么?包内容显示该请求想知道:谁的IP是192.168.26.3,也就是,谁是主机B?<ul><li>在A处重复流程1中的IP地址与网络掩码的<strong>按位与</strong>运算,不过这次的网络掩码要用主机A的掩码,发现:A对应的网络地址为192.168.26.0,而B对应的网络地址竟然也是192.168.26.0!</li><li>你为什么会这么震惊?</li><li>你是不是发现了什么不得了的事儿?</li><li>没错,你已经知道了另一个事实:在A看来,本机与目的主机B在同一网段!从而A要主动向B发包的话,在知道其MAC地址的前提下,是可以直接发送而无需默认网关转发的。但好像又有不对劲的地方…容我反应一下…</li><li>你马上想到一个问题:A为什么会要给B发包?</li><li>我猜你问出这个问题的下一秒,已经知道了答案:当然是因为A已经收到了B的ping包,而A在想要回复B一个ICMP包时候发现自己并不知道主机B的MAC地址呀!</li><li>这间接证明了在流程3中,默认网关确实将B发送给它的那个目的IP地址为A的IP地址的ICMP包,正确地转发给了A。<br><br></li></ul></li><li>至于<strong>第六个数据包</strong>,当然是主机B回复主机A的ARP请求的ARP Reply,将自己MAC地址告诉主机A,方便主机A响应此前自己的ping操作。<ul><li>这就又证明了一件事儿:主机B在执行ARP回复时并不考虑子网,这不,虽然ARP请求在它看来,来自另一个子网的IP地址,但也照样回复。<br><br></li></ul></li><li>之后,主机B终于收到了来自A的ICMP Reply回包,即在抓到<strong>第七个相关数据包</strong>后,一次ping过程成功完成。<br><br></li><li>后来的过程中,主机A、B由于清楚地知道了对方的联系方式(各自以及默认网关的IP地址、MAC地址以及路由信息),此后的就没必要再发ARP包了,于是此后都是直接抓到双方由于ping产生ICMP数据包。<br><br></li></ol><p>分析完这几个包,面试题的答案有了,原来通信流程是这样:B先把ping请求交给默认网关,默认网关再转发给A,而A收到请求后直接把ping回复给B,形成如下所示的三角环路:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/Mechanism/subnet/triangle-loop.png" alt="image"></p><p>让我们再回到之前的那个问题:为什么以<strong>B ping A</strong>为例,而不是<strong>A ping B</strong>呢?</p><p>其实答案已经一目了然:<strong>A ping B</strong>时,A通过子网掩码运算后发现B和自己在同一网段,广播ARP请求从来自主机B的ARP应答报文中获取其MAC地址,于是直接发送ICMP Request包给B。此后B进行回复,于是重复上述步骤1~4,随后默认网关将此ICMP Reply包转发至A,完成一次ping过程。</p><p>以上,算是完整解答了面试题的各种情况,但我们的目的绝不仅限于此,不是吗?</p><p><strong>坚持积累的同时,也要及时的总结。</strong></p><p>将零散的知识点经过总结后才能更容易整合到自己的技术体系中,会更有利于记忆和掌握,接下来我们好好掰扯掰扯子网这个东西。</p><h2 id="子网掩码"><a href="#子网掩码" class="headerlink" title="子网掩码"></a>子网掩码</h2><hr><h3 id="功能"><a href="#功能" class="headerlink" title="功能"></a>功能</h3><p>IP地址由网络号和主机号组成,网络号标识的是Internet(互联网)中的一个网段/子网,而主机号标识的子网中的某台主机。网际地址分解成两个域(Domain)之后带来了一个很明显的好处:</p><blockquote><p>IP数据包从网际中的一个网络到达另一个网络时,路由器转发时选择路径可以基于网络而不是基于主机。在大型的网络中,这一点优势尤为突出,因为连接不同网络的路由表中只需存储网络信息,而不是主机信息,这样无疑简化了路由表,极大减轻了路由器的存储开销(Storage Overhead)。</p></blockquote><p>看到这里,你大概会问,子网掩码在其中扮演了什么样的角色呢?</p><p>利用子网掩码就可以将一个IP地址分割为网络地址和主机地址两个域,具体作用体现在以下两方面:</p><ol><li>屏蔽IP地址的一部分以区别网络号和主机号;</li><li>进一步划分网络,得到同一网络下的若干子网(Subnet)。</li></ol><p>对应以上作用1,有如下IP地址组成<strong>二层结构</strong>等式:</p><ul><li>IP 地址 = 网络地址(Network Address) + 主机地址(Host Address)</li></ul><p>以及,对应以上作用2,即如果进一步<strong>划分子网(Subnetting</strong>),IP地址变成<strong>三层结构</strong>:</p><ul><li>IP地址 = 网络地址(Network Address) + <strong>子网地址(Subnet Address) </strong>+ 主机地址(Host Address)</li></ul><h3 id="原理"><a href="#原理" class="headerlink" title="原理"></a>原理</h3><p>那么子网掩码是如何实现上述功能的呢?</p><p>子网掩码包含32位,通过将网络位置为“1”,主机位置为“0”,完成网络地址与主机地址的分割功能,此时IP地址具有二层结构:</p><ul><li>IP地址 = 网络地址+主机地址</li></ul><p>任何设备IP地址与其子网掩码按位与后的结果就可以得到其网络地址,具体到路由器上,每个需要其转发的数据包到达后,路由器使用将该包的目的IP地址与其对应的子网掩码按位与的方法,得出该目的IP地址所在网段,然后查询路由表,将数据包往匹配表项中指定的下一跳(Next Hop)进行转发。</p><p>此外,子网掩码还能帮助我们,将一个大型的IP网络,进一步细分为若干子网,这一过程叫作子网划分(Subnetting),通过向IP地址中原二层结构中的主机地址<strong>借用</strong>若干位作为子网位(Subnet ID),主机位减少位数,即可将一个较大的网络细分为 <strong>2^子网位数</strong> 个小型子网,此时IP地址具有三层结构:</p><ul><li>IP地址 = 网络地址 + 子网地址 +主机地址</li></ul><p>并通过比较源IP及目的IP地址分别与本机子网掩码按位与后的结果是否相同,判断目的主机是否在局域网内(相同),或在远程网上(不同)。</p><h2 id="无类别域间路由"><a href="#无类别域间路由" class="headerlink" title="无类别域间路由"></a>无类别域间路由</h2><hr><p>无类别域间路由,即Classless InterDomain Routing,简称CIDR,该机制的引入是为了应对IPv4地址会被耗尽这个问题,注意,应对(Cope with),或者说缓解(Mitigate),但不是解决(Solve)。</p><p>IPv4地址总共32位,最多支持2^32=4294967296个IP地址,而地址分类机制,即将IP地址分为A、B、C等几个类别,每次划分时以8位作为递增步长,只支持8、16和24位的网络地址,而由于管理机构在为申请组织分配IP地址时,一般都是以网络为单位分配,即每次会将一整个网络的IP地址全部分配下发出去,这种分配机制十分不灵活,甚至会带来相当严重的浪费问题。</p><p>具体举例如下:</p><ol><li><p>一个组织在申请网络地址时一次性最少也可以被分配到256个IP地址,此时对应C类地址,主机位数为8,假如这是家小公司,总共只需要15个独立IP,且无论短期还是长期都没有扩充机器数量的计划,这无疑就造成严重的地址浪费。</p></li><li><p>如果一个组织需要超过256个IP地址,假设1000个,那么管理机构就必须为其分配B类地址,而B类地址支持65536个独立IP地址,同样出现极大的地址浪费现象。</p></li><li><p>该组织需要超过65536个独立IP地址呢?为其分配A类地址,浪费更加不可想象!</p></li></ol><p>出于安全等多方面的考虑,分配地址仍只能以网络为单位,故这种分配机制的不灵活性和浪费问题无法从根本上解决,CIDR就是提供了一种缓和机制:划分IP地址时以1位作为递增步长,这样不仅支持8、16、24三种网络位数,网络位数位小于32位的任意位数都为合法IP地址,这时的网络位数还是由子网掩码来确定。</p><p>下图展示了将子网掩码应用于A、B、C等类别(Classful)网络,以及无类别(Classless)网络的情况:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/ Network/Mechanism/subnet/subnet-mask-example-part1.png" alt="image"><br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/Mechanism/subnet/subnet-mask-example-part2.png" alt="image"></p><p>当然,CIDR机制仅仅只是缓和了IPv4地址消耗过快的事实,使得IP地址能够以一种更加高校的方式被管理结构分配给网络服务提供商(Internet Service Provider,ISP)和用户,但是正如上文所说,随着互联网设备和用户规模与日俱增,这“治标不治本”。到目前为止,要想真正解决网络地址短缺的问题,及早普及在1993年引入的IPv6协议才是终极方法,在IPv6协议下,网络地址为IPv6地址,包含128位(bit),支持的独立地址数量达到了2^128这一根本无法企及的天文数字。</p><h2 id="参考资源"><a href="#参考资源" class="headerlink" title="参考资源"></a>参考资源</h2><hr><p>[1] <a href="https://www.iplocation.net/subnet-mask" target="_blank" rel="noopener">What is a Subnet Mask</a><br>[2]<a href="https://www.zhihu.com/question/21064101" target="_blank" rel="noopener">如何理解子网掩码中的“子网”</a><br>[3]<a href="https://wizardforcel.gitbooks.io/network-basic/content/6.html" target="_blank" rel="noopener">网络基本功:细说IP地址与子网</a></p>]]></content>
<categories>
<category> Network </category>
</categories>
<tags>
<tag> Subnet </tag>
<tag> Wireshark </tag>
</tags>
</entry>
<entry>
<title>转发表(MAC表)、ARP表、路由表总结</title>
<link href="/2017/11/15/Network/Mechanism/mac-arp-routing-table/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><hr><p>计算机网络中一个关键步骤在于通信路径上不同节点对于流经本节点的数据包转发,常见的交换设备主要是交换机(第二层、三层)和路由器(第三层),在实际运行时,它们各自维护一些表结构帮助完成数据包的正确寻址与转发,本文详细介绍了三张至关重要的表:转发表、ARP表与路由表的在网络数据包转发功能中发挥的作用,以及它们协同工作的原理,顺便也会接着<a href="http://dongdongdong.me/2017/10/30/Network/Hardware/relay/"><strong>之前的文章</strong></a>继续谈谈交换机和路由器的一些事儿。</p><a id="more"></a><h2 id="网络分层协议"><a href="#网络分层协议" class="headerlink" title="网络分层协议"></a>网络分层协议</h2><hr><p>计算机网络是将地理上隔离的计算节点从物理上相连(双绞线、光纤、无线信号等),并采用分层方式,将计算机网络自顶向下依次分为五层:应用层->传输层->网络层->数据链路层->物理层,对应设计有一整套网络协议栈,各层次分别运行有多种协议,下层向紧邻上层提供服务并隐藏通信细节,上层调用紧邻下层提供的服务,完成主机不同层次之间的对等通信,高层(应用层、传输层、网络层)之间借此实现逻辑相连通信。之所以称为“逻辑通信”,是因为不同主机相同的较高层次之间好像是沿水平方向传送数据,但事实上这两个对等层次之间并没有一条水平方向的物理连接,举例说明如下:</p><ol><li><strong>传输层协议</strong>用于实现<strong>应用进程间</strong>逻辑通信,这是因为<strong>进程</strong>本来就是便于资源分配与管理而发明的<strong>逻辑概念</strong>,并不是<strong>物理实体</strong>,无法从物理上产生关联;</li><li><p><strong>网络层协议</strong>用于实现<strong>主机间</strong>逻辑通信,这是因为两个<strong>通信主机</strong>通常位处网络拓扑中的不同位置,大多时候并非直接从物理上相连,之间需要一条根据<strong>路由协议</strong>选路确定的通信路径,网络层协议负责数据包最终能够发送给目的主机,但向上(即给运输层)只提供尽力而为(Best-Effort)的数据包服务;</p></li><li><p><strong>数据链路层</strong>用于实现<strong>点到点通信</strong>,由于不同节点在物理上通过真实的数据链路相连,不能再算是逻辑通信。</p></li></ol><h2 id="转发表-Forwarding-Table"><a href="#转发表-Forwarding-Table" class="headerlink" title="转发表(Forwarding Table)"></a>转发表(Forwarding Table)</h2><hr><h3 id="MAC地址"><a href="#MAC地址" class="headerlink" title="MAC地址"></a>MAC地址</h3><p>MAC地址(Media Access Control Address),即媒体访问控制地址,通常也称为以太网地址或物理地址,它是一个用于确认网络设备位置的地址,每个网络设备(如网络适配器(Network Adapter),即网卡(Network Interface Card, NIC))都有世上唯一的MAC地址,一台设备若有多张网卡,则每个网卡都必须具有一个唯一的MAC地址,这个是在网络设备出厂时由厂商烧制确定。</p><p>MAC地址共48位,即6个字节,通常每4位构成一个16进制数,从而可以表示成xx:xx:xx:xx:xx:xx的形式,每个x都是一个16进制数。其中ff:ff:ff:ff:ff:ff为广播地址,以此作为目的地址的数据包会被交换机广播至全部端口,发到与其端口相连的全部局域网;01:xx:xx:xx:xx:xx是多播地址。</p><h3 id="工作流"><a href="#工作流" class="headerlink" title="工作流"></a>工作流</h3><p>转发表,又称MAC表,聊到它就不得不提到交换机里,因为交换机就是根据转发表来转发数据帧的。</p><p>交换机本质上也是一个计算机,拥有计算(CPU)、存储(SRAM或TCAM)和网络资源(转发芯片和链路),甚至还会安装专用操作系统,它会维护一张记录着局域网主机端口MAC地址与交换机端口对应的表,交换机就是根据这张表负责将数据帧传输到指定的主机端口上的。</p><p>交换机具有“<strong>存储转发</strong>”功能:</p><ul><li><p>交换机在接收到数据帧以后,首先会记录数据帧中的源MAC地址和对应的到达端口到MAC表中,这一过程通常称为“自学习”,不需要任何的人工干预;</p></li><li><p>接着,交换机检查自己的MAC表是否有数据帧中目的MAC地址的匹配条目,如果有,则会根据MAC表中记录的对应端口将数据帧转发出去,这一转发方式称为“单播”(Unicast)。而如果没有,则会将该数据帧从非达到端口的其它全部端口发送出去,这一转发方式程序称为“广播”(Broadcast)。</p></li></ul><p>下面会以图示的方式详细讲解交换机传输数据帧的过程,下面先来看看单个交换机转发的情形:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/Mechanism/mac-arp-routing-table/forwarding-single-switch.png" alt="image"></p><p>步骤如下:</p><ol><li>主机A会将一个源MAC地址为本机网卡物理地址,目的MAC地址为主机B网卡物理地址的数据帧发送给交换机1;</li><li>交换机收到此数据帧后,首先将数据帧中的源MAC地址和对应的输入端口0记录到交换机维护的MAC地址表中;</li><li>然后交换机会检查自己的MAC地址表中是否有数据帧中的目的MAC地址的信息,如果有,则从MAC地址表中记录的对应端口发送出去,如果没有,则会将此数据帧从非接收端口的所有端口发送出去,这里仅从端口1发出;</li><li>这时,局域网的所有主机(通过交换机相连的全部主机)都会收到此数据帧,但是只有主机B收到此数据帧时会响应这个广播帧,并回应一个数据帧(这个回应由什么机制确定的,需要再查资料确定一下~TCP的ACK包?),此数据帧中包括主机B网络设备的MAC地址;</li><li>当交换机收到主机B回应的数据帧后,也会记录数据帧中的源MAC地址,即主机B网络设备的MAC地址,这时,再当主机A和主机B相互通信时,交换机就根据MAC地址表中的记录,实现单播了,一趟转发流程实际交换机就“学习”到了两个转发表条目。</li></ol><p>那么当局域网内存在多个交换机互连时,交换机的MAC地址表要如何记录呢?下图就展示了该种情形:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/Mechanism/mac-arp-routing-table/forwarding-multiple-switch.png" alt="image"></p><p>步骤如下:</p><ol><li>主机A将一个源MAC地址为本机网卡物理地址,目的MAC地址为主机C网卡物理地址的数据帧发送给交换机1;</li><li>交换机1收到此数据帧后,会学习源MAC地址,并检查MAC地址表,发现没有目的MAC地址的记录,则会将数据帧广播出去,主机B和交换机2都会收到此数据帧;</li><li>交换机2收到此数据帧后也会将数据帧中的源MAC地址和对应的端口记录到MAC地址表中,并检查自己的MAC地址表,发现没有目的MAC地址的记录,则会广播此数据帧,主机C和主机D都会收到此数据帧;</li><li>主机C收到数据帧后,会响应这个数据帧,并回复一个源MAC地址为本机网卡物理地址的数据帧,该帧最终会送往主机A,这时交换机1和交换机2都会将主机C的MAC地址记录到自己的MAC地址表中,并且以单播的形式将此数据帧发送给主机A;</li><li>这时,主机A和主机C通信就可以以单播的形式传输数据帧了,A与D、B与C及B与D的通信与上述过程一样,因此交换机2的MAC地址表中记录着主机A和主机B的MAC地址都对应其端口3。</li></ol><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>从以上两幅图可以看出,交换机具有动态自学习源MAC地址与物理端口映射的功能,并且交换机的一个端口可以对应多个MAC地址,但是一个MAC地址只能对应一个端口。</p><p>注:交换机动态学习的MAC地址默认只有300s的有效期,如果300s内记录的MAC地址没有对应的通信过程来更新对应条目,则会自动删除此记录,这是由交换机中的一个计时器所维护的。</p><h2 id="ARP表-Address-Resolution-Table"><a href="#ARP表-Address-Resolution-Table" class="headerlink" title="ARP表(Address Resolution Table)"></a>ARP表(Address Resolution Table)</h2><hr><h3 id="ARP协议"><a href="#ARP协议" class="headerlink" title="ARP协议"></a>ARP协议</h3><p>首先明确一点,<strong>在以太网环境下,同一个网段的主机之间需要知道对方的MAC地址,才能进行通信</strong>。</p><p>上一节介绍了交换机的工作原理,了解到交换机是根据MAC寻址,查表确认输出端口以完成本节点转发任务的。看到这里其实应该可以抛出从一开始就被我们忽视了的问题:在初始构造数据包准备发送时,源主机究竟要如何获得目的主机网络设备MAC地址的呢?这时,就需要使用到ARP协议。在网络拓扑中的每个节点或说主机上,实际都维护有一张ARP表,它记录着主机的IP地址(网络地址)到MAC地址(物理地址)的映射关系。</p><p>ARP协议,即地址解析协议,它是一个网络层协议,运行在各网络节点上,负责完成主机IP地址到MAC地址的映射。</p><h3 id="工作流-1"><a href="#工作流-1" class="headerlink" title="工作流"></a>工作流</h3><p>接下来根据下图,详细讲解一下ARP协议的工作原理:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/Mechanism/mac-arp-routing-table/example-topology-arp.png" alt="image"></p><p>步骤如下:</p><ol><li>如果主机A想发送数据包给同一网段内的另一台主机B(通过交换机相连的节点处于同一网段),很明显,A的用户应用程序要么已经知道B的IP地址,或者说域名(Domain Name,DNS协议会完成主机名到IP地址的映射,这里不是重点),那么主机A首先会检查自己的ARP缓存表(ARP Cache),查看是否有主机B的IP地址与其MAC地址的对应关系,如果有,则直接将主机B网络设备的MAC地址作为目的MAC地址封装到数据帧中,无需进一步操作即获取到数据帧封装所需的全部信息,此后完成封装并发送数据帧到目的MAC地址。如果没有,主机A则会发送一个ARP请求信息(ARP Request),请求的目的IP地址是主机B的IP地址,目的MAC地址是MAC层的广播地址(即ff:ff:ff:ff:ff:ff),源IP地址和MAC地址是主机A的IP地址及其MAC地址;</li><li>当交换机接收到此数据帧之后,发现此帧是广播帧,因此,会将此数据帧从非接收的所有端口发送出去;</li><li>同一网段中的所有节点都会接收到该ARP请求数据包,目的IP不匹配的节点会直接忽略该请求,而当主机B接收到此数据帧后,解析到IP地址与自己的IP地址一致,先将主机A的IP地址及其MAC地址的对应关系记录到自己的ARP缓存表中,同时会发送一个ARP应答(ARP Response),应答数据包源MAC地址即主机B自身网络设备的MAC地址,该应答通过交换机转发至主机A;</li><li>主机A在收到这个回应的数据帧之后,在自己的ARP缓存表中记录主机B的IP地址和MAC地址的对应关系。而此时主机A已经可以继续封装准备发往主机B的数据帧,而交换机也已经学习到了主机A和主机B的MAC地址与其端口的对应关系,之后主机A发送的数据帧通过交换机转发至主机B。<br><br></li></ol><p><strong>这里有一点值得注意的是</strong>传输过程中IP地址与MAC的地址的变化问题:</p><ul><li>MAC地址在同一个广播域中传播时始终不变,但在跨越广播域(即经过路由器)时,会由于重新封装而改变,源MAC地址将变为路由器的输出端口的MAC地址,目的MAC地址随网络拓扑实际情况而定,若路由器与目的主机所在网段直连,此时目的MAC地址就是目的主机的MAC地址;而不管是源IP地址还是目的IP地址,在数据包传输过程中都始终不会改变。</li></ul><!--主机A在构造数据帧准备发送给主机B时,源IP地址和源MAC地址分别是自己的IP地址和MAC地址,但是目的IP地址和目的MAC地址则应该是主机B的对应地址,但是由于A与B不是直接通过链路相连,通信路径上还需经过若干转发设备,如交换机,因为MAC层即数据链路层只负责链路两端点之间的数据传输,且交换机通常具有“存储转发”功能,即其在接收到一个数据包后会将其短暂存储,历经解包和再次打包成帧(Framing)的过程,根据其上维护的MAC表查到对应目的MAC地址的输出端口将其转发。此时转发出去的数据帧有效载荷(Payload, 数据帧=以太网头部+网络层数据报,即帧有效载荷实际就是网络层数据报)不会改变,这是因为交换机工作在数据链路层,在解析数据包的时候不会涉及到网络层及以上层次的协议字段。但是由于重新打包步骤的存在,导致以太网头部中的源MAC地址将会被交换机修改为其自身的硬件MAC地址,目的MAC地址则依然不会改变,之后从输出端口推向链路进行传输。--><h3 id="小结-1"><a href="#小结-1" class="headerlink" title="小结"></a>小结</h3><p>网络中每个节点都会通过运行ARP协议而维护节点内部的一张ARP缓存表,用于完成从IP地址到MAC地址的映射,在发送数据之前往往会先查询本地ARP表中对应目的IP地址的MAC地址,若没有表项则会发起ARP广播请求直至获取对应主机响应并发来应答,将该应答中包含的目的IP地址与MAC地址的映射关系添加到ARP缓存表之后,在数据链路层就可以以该MAC地址为目的MAC地址封装数据帧并发送。</p><h2 id="路由表-Routing-Table"><a href="#路由表-Routing-Table" class="headerlink" title="路由表(Routing Table)"></a>路由表(Routing Table)</h2><hr><h3 id="IP地址"><a href="#IP地址" class="headerlink" title="IP地址"></a>IP地址</h3><p>IP地址(Internet Protocol Address),即互联网协议地址,也称为网络层地址或主机地址,是分配给网络上的各个网络设备的地址。</p><p>现在流行的IP协议有两个版本:IPv4(Internet Protocol Version 4)和IPv6(Internet Protocol Version 6)。其中,IPv4地址为32位,即4个字节,为便于使用,常以xxx.xxx.xxx.xxx每个字节8位从二进制表示为十进制数,这种表示方法称为点分十进制,地址可分为A、B、C、D、E五大类,32位全为1的IP地址:255.255.255.255称为“受限广播地址”(Limited Broadcasr Destination Address),用于将一个分组以广播方式发送给本网络中的所有主机,路由器则阻挡该分组通过,将其广播功能限制在本网内部,因此可以说路由器隔离了广播域(交换机隔离了冲突域)。</p><p>随着网络规模和节点数量的不断扩展,出于32位的IPv4将很快被分配使用殆尽的担忧,又推出了IPv6地址,128位,16个字节,通常每4位表示为一个16进制数,16个字节分为8组,每组包含2个字节即4个16进制数,组与组之间以冒号分割:ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff为IPv6协议下的广播地址。</p><h3 id="IP-VS-MAC"><a href="#IP-VS-MAC" class="headerlink" title="IP VS MAC"></a>IP VS MAC</h3><p>MAC地址和IP地址本质都是网络设备物理端口的性质,都可用于寻址网络设备,但如果它们功能类似,仅是工作的层次不同的话,两者之间应该可以通过某种机制实现相互替换,但为什么它们能共存至今呢?</p><p>对于MAC地址和IP地址的比较,有人曾这么比喻:一个人已经有了手机号(IP地址),为什么还要有身份证号呢(MAC地址)?身份证号是一个人的唯一标识号,只要有这个号码就能找到该人,但是为啥他的朋友们不用身份证号来寻找该人而用手机号呢?对,因为方便。但是如果该人犯罪,警察用手机号呼叫寻找该人,傻子才会接吧…而且换了号码怎么办?那不就得用你的身份证号在全国发布通缉令,利用身份证号在全国各种系统范围(消费、社交、医疗)内搜索该人的记录,才能最终定位该人实施有效逮捕嘛。</p><p>上述例子实际就契合了两种地址存在的意义:IP地址是逻辑地址,根据网络协议,在不同的地理位置加入互联网就会分配到完全不同的IP地址(DHCP动态分配IP地址),但由于属于网络层,相对较高的抽象层次设计的初衷就是为了简化通信,方便使用,尤其对于用户进程而言;MAC地址是物理地址,工作在数据链路层,一旦出厂时由厂商确定并烧制入网络设备的EPROM中就具有了固定的全球唯一的地址,任何时候任何条件都不会改变,虽说使用起来不太方便,且描述的是较低层的数据链路通信细节,但在任何时候都可用于数据通信寻址。</p><p>更严谨完善的解释如下:</p><blockquote><p>问题:既然每个以太网设备在出厂时都有一个唯一的MAC地址了,那为什么还需要为每台主机再分配一个IP地址呢?或者说为什么每台主机都分配唯一的IP地址了,为什么还要在网络设备(如网卡,集线器,路由器等)生产时内嵌一个唯一的MAC地址呢?</p><p>解答:主要原因基于以下几点:<br>(1)IP地址的分配是根据网络的拓扑结构,而不是根据谁制造了网络设置。若将高效的路由选择方案建立在设备制造商的基础上而不是网络所处的拓扑位置基础上,这种方案是不可行的;<br>(2)当存在一个附加层的地址寻址时,设备更易于移动和维修。例如,如果一个以太网卡坏了,可以被更换,而无须取得一个新的IP地址。如果一个IP主机从一个网络移到另一个网络,可以给它一个新的IP地址,而无须换一个新的网卡;<br>(3)无论是局域网,还是广域网中的计算机之间的通信,最终都表现为将数据包从某种形式的链路上的初始节点出发,从一个节点传递到另一个节点,最终传送到目的节点。数据包在这些节点之间的移动都是由ARP协议负责将IP地址映射到MAC地址上来完成的。</p></blockquote><p>下面再通过一个例子看看IP地址和MAC地址是怎样结合起来传送数据包的:</p><blockquote><p>假设网络上要将一个数据包(名为PAC)由北京的一台主机(名称为A,IP地址为IP_A,MAC地址为MAC_A)发送到纽约的一台主机(名称为B,IP地址为IP_B,MAC地址为MAC_B)。这两台主机之间不太可能是直连起来的,因而数据包在传递时必然要经过许多中间节点(如路由器,网关服务器等),假定在传输过程中要经过C1、C2、C3(其输入、输出端口的MAC地址分别为M1_In/M1_Out,M2_In/M2_Out,M3_In/M3_Out)三个节点。A在将PAC发出之前,先发送一个ARP请求,找到其要到达IP_B所必须经历的第一个中间节点C1的到达端口MAC地址M1_In,然后在其数据包中封装地址:IP_A、IP_B,MAC_A和M1_In。当PAC传到C1后,再由ARP根据其目的IP地址IP_B,找到其要经历的第二个中间节点C2的到达端口MAC地址M2_In,然后再封装目的MAC地址为M2_Out的数据包传送到C2。如此类推,直到最后找到IP地址为IP_B的B主机的MAC地址MAC_B,最终传送给主机B。在传输过程中,数据包源IP地址IP_A、目的IP地址IP_B不变,而源MAC地址和目的MAC地址,由于中间节点重新封装数据帧而不断改变,直至目的地址MAC地址为MAC_B,数据包最终到达目的主机B。</p></blockquote><p>综上所述,IP地址和MAC地址相同点是它们都可以作为设备地址标识,不同则主要体现以下几个方面:</p><ol><li>对于网络上的某一设备,如一台计算机或一台路由器,其IP地址可变(但必须唯一),而MAC地址不可变。我们可以根据需要给一台主机指定任意的IP地址,如我们可以给局域网上的某台计算机分配IP地址为192.168.0.112 ,也可以将它改成192.168.0.200。而任一网络设备(如网卡,路由器)一旦生产出来以后,其MAC地址永远唯一且不能由用户改变;</li><li>长度不同。IP地址为32位4字节,MAC地址为48位6字节;</li><li>分配依据不同。IP地址的分配是<strong>基于网络拓朴</strong>,MAC地址的分配是<strong>基于制造商</strong>;</li><li>寻址协议层不同。IP地址应用于OSI第三层,即网络层,而MAC地址应用在OSI第二层,即数据链路层。 数据链路层协议可以使数据从一个节点传递到同一段链路的另一个节点上(通过MAC地址寻址),而网络层协议使数据可以从一个网络传递到另一个网络上(ARP根据目的IP地址,找到中间节点的MAC地址,通过中间节点转发,从而最终到达目的网络)。</li></ol><h3 id="工作流-2"><a href="#工作流-2" class="headerlink" title="工作流"></a>工作流</h3><p>路由器负责不同网段(Subnet, 子网)之间的通信,每个与路由器端口相连的网络被称为一个子网或网段,也就是一个广播域。在路由器中也有一张表,这张表叫做路由表,通过在网络节点上运行路由协议,记录并更新去往不同网段的路径信息。路由表中的信息分为直连路由和非直连路由:</p><ul><li><strong>直连路由</strong>:直接连接到路由器端口的网段,该信息由路由器自动生成;</li><li><strong>非直连路由</strong>:不是直接连接到路由器端口的网段,此记录需要手动添加或使用动态路由生成。</li></ul><p>本机Linux系统下, 双网卡em1(114.212.84.179)与virbr0(192.168.122.1),执行命令:route -n,获取到内核IP路由表的数值展示形式如下:</p><style>table th:nth-of-type(1) { width: 100px;}</style><style>table th:nth-of-type(2) { width: 100px;}</style><style>table th:nth-of-type(3) { width: 100px;}</style><table><thead><tr><th>Destination</th><th>Gateway</th><th>Genmask</th><th>Flags</th><th>Metric</th><th>Ref</th><th>Use</th><th>Iface</th></tr></thead><tbody><tr><td>114.212.80.0</td><td>0.0.0.0</td><td>255.255.248.0</td><td>U</td><td>100</td><td>0</td><td>0</td><td>em1</td></tr><tr><td>192.168.122.0</td><td>0.0.0.0</td><td>255.255.255.0</td><td>U</td><td>0</td><td>0</td><td>0</td><td>virbr0</td></tr><tr><td>0.0.0.0</td><td>114.212.80.1</td><td>0.0.0.0</td><td>UG</td><td>100</td><td>0</td><td>0</td><td>em1</td></tr></tbody></table><p>解析上述路由表:</p><blockquote><p>第一项:目的网络为114.212.80.0/21,网关地址为0.0.0.0(“<em>.</em>.<em>.</em>“的数值形式),这表示该网段属于与路由器某端口直连的网段,数据包将从路由器em1接口输出;</p><p>第二项:目的网络为192.168.122.0/24,网关地址同样为0.0.0.0,表示该网段属于与路由器某端口直连的网段,数据包将从路由器virbr0接口输出;</p><p>第三项:目的网络为0.0.0.0(“default”的数值形式)时,即匹配任意网段,由于路由表匹配执行最先匹配策略,第三项就是当目的IP地址无法与前两项匹配时的成功匹配项,对应的网关称为默认网关,即路由器中没有存储某个目的网络的表项时应该转发至的下一跳地址,从em1端口输出。</p></blockquote><p>路由器中记录的条目有的需要手动添加,称为静态路由;有的则是动态获取的,称为动态路由。表中的每个条目都有以下属性:</p><ol><li><p><strong>目的网络地址(Destination)</strong>:网络地址和网络掩码相与的结果用于定义本机可以达到的目的网络范围,通常情况下,目的网络范围包含以下几种情况:<br> (1) <strong>主机地址</strong>:某个特定主机的网络地址;<br> (2) <strong>子网地址</strong>:某个特定子网的网络地址;<br> (3) <strong>默认路由</strong>:所有未在路由表中指定的网络地址,用0.0.0.0统一匹配,用于配置默认网关;</p></li><li><p><strong>网络掩码(Genmask)</strong>:又称为子网掩码(Subnet Mask),是一个32位地址,作用是将一个同样也是32位的IPv4地址划分成网络地址(Network Address)和主机地址(Host Address)。子网掩码不能单独存在,它必须结合IP地址一起使用。子网掩码是用来判断任意两台主机是否处于同一网段的根据,简单来说就是两台主机各自的IP地址与本机配置的子网掩码做按位与操作,如果结果相同,则说明这两台主机是处于同一网段,可以进行直接的通讯,而无需路由器的转发;</p></li><li><p><strong>网关(Gateway,又被称为下一跳服务器(Next Hop Server))</strong>:发送IP数据包时,网关定义了针对特定的网络目的地址,数据包将要被发送到的下一跳IP地址。如果是与路由器直接相连的网段,网关通常就是路由器对应的网络端口的IP地址,但是此时接口必须与网关一致。如果是远程网络或默认路由,网关通常是与路由器相连网络上的某个服务器或路由器。如果目标是本主机所属网络,不需要路由,网关显示为”*“;</p></li><li><p><strong>接口(Iface)</strong>:接口定义了针对特定的网络目的地址,路由器用于转发数据包的网络接口(路由器的物理端口)。网关必须位于和接口相同的子网(默认网关除外),否则造成在使用此路由项时需调用其他路由项,从而可能会导致路由死锁;</p></li><li><p><strong>跳数(Metric)</strong>:跳数用于指出路由的成本,通常情况下代表到达目标地址所需要的总跳数,一个跳数代表经过一个路由器,IP数据报首部中的TTL字段就是该数据报所能存活的总跳数。跳数越少往往代表着该路由成本越低,跳数越多则说明成本越高。当具有多条达到相同目的网络的路由选项时,路由算法会选择具有更少跳数的路由。</p></li><li><p><strong>标志(Flags)</strong>:多种路由表项标记含义如下:<br> (1) U:路由是动态的;<br> (2) H:目标是一个主机;<br> (3) G:路由指向网关;<br> (4) R:恢复动态路由产生的表项;<br> (5) D:由路由的后台程序动态安装;<br> (6) M:由路由的后台程序修改;<br> (7) !:拒绝路由。</p></li><li><p><strong>引用次数(Refs)</strong>:Linux内核中未使用,一般是0;</p></li><li><p><strong>查找次数(Use)</strong>:此路由项被路由软件查找的次数。</p></li></ol><p>路由器是工作在网络层的,在网络层可以识别逻辑地址,即IP地址,也就是说数据包解析时最多可将数据帧拆包成IP数据包,路由器无法操作数据报的载荷字段,但是可以针对IP首部做些事情:当路由器的某个端口收到一个包时,路由器就会读取包中的目地IP地址,然后在路由表中进行查找。如果在路由表中找到目的IP地址对应条目,则把包转发到路由器的对应端口。如果没找到,那么如果路由器配置默认路由(默认网关),就默认将所有无法解析的目的网段主机的数据包都先发往该默认网关做进一步转发,如果没有配置默认路由,则将该包丢弃,并返回源主机以不可达(Unreachable)的信息。这就是数据包路由的过程。</p><p>利用下图详细介绍路由器的工作原理:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/Mechanism/mac-arp-routing-table/example-topology-router.png" alt="image"></p><p>步骤如下:</p><ol><li>主机A在网络层将来自上层的报文封装成IP数据报,IP首部中的源IP地址为自己的IP地址,目的IP地址为主机B的IP地址。主机A会用本机配置的24位子网掩码与目的地址进行“与”运算,得出目的地址与本机不在同一个网段(主机A位于192.168.1.0/24网段,主机B位于192.168.2.0/24网段,或称属于不同子网),因此发送给主机B的数据包需要经过网关路由器1的转发;</li><li>主机A通过ARP请求获得网关路由器1的E0端口的MAC地址,并在数据链路层将路由器E0端口的MAC地址封装成以太网帧首部中的目的MAC地址,源MAC地址是自己的MAC地址,随后发送数据帧给路由器1;</li><li>路由器1从端口E0接收到该数据帧,随后执行解析,将数据链路层的首部剥落去掉,并在路由表中检查是否有目的IP地址的网段对应表项(即192.168.2.2/24和其所在192.168.2.0/24网段),根据路由表中记录,发往192.168.2.0/24网段(中主机)的数据包的下一跳(Next Hop)或称网关地址为10.1.1.2/8(实际是路由器2的E1端口的IP地址),而路由器发现下一跳地址正好位处与自己E1端口直连的网段(10.0.0.0/8),于是数据在路由器1的E1端口重新封装,此时,以太网帧的源MAC地址是路由器1的E1端口的MAC地址,目的MAC地址则是路由器2的E1端口的MAC地址,通过ARP广播得到,封装完毕发送数据帧给路由器2;</li><li>路由器2从端口E1接收到该数据帧,随后执行解析,将数据链路层的首部剥落去掉,对目的IP地址进行检测,并与路由表进行匹配,此时发现目的主机IP地址所在网段正好是自己E0端口的直连网段,路由器2于是通过ARP广播,获知主机B的MAC地址,此时数据包在路由器2的E0端口再次封装,源MAC地址是路由器2的E0端口的MAC地址,目的MAC地址是主机B的MAC地址,随后发送数据帧给主机B;</li><li>完成以上1~4,主机B终于接收到来自主机A的数据包。</li></ol><p>综上,看似较为“简单”的跨网段主机通信,真也不是太容易的一件事儿。</p><h3 id="小结-2"><a href="#小结-2" class="headerlink" title="小结"></a>小结</h3><p>路由表负责记录一个网络到另一个网络的路径,路由器依赖路由协议及其确定的路由表完成三层,即网络层的数据转发工作。路由表项中最重要的信息在于目的网段和网关,即下一跳IP地址的对应关系,网关通常是专门的网关服务器或者路由器,而网关会负责将该数据包最终转发至目的网段。</p><h2 id="参考资源"><a href="#参考资源" class="headerlink" title="参考资源"></a>参考资源</h2><hr><p>[1] <a href="http://dengqi.blog.51cto.com/5685776/1223132" target="_blank" rel="noopener">详解网络传输中的三张表,MAC地址表、ARP缓存表以及路由表</a><br>[2] <a href="http://blog.csdn.net/wangerge/article/details/3931491" target="_blank" rel="noopener">单播、多播(组播)和广播的区别</a><br>[3] <a href="http://blog.csdn.net/zyboy2000/article/details/50528253" target="_blank" rel="noopener">路由表详解</a><br>[4] <a href="http://www.ywnds.com/?p=1269" target="_blank" rel="noopener">传输层协议概述</a><br>[5] <a href="http://blog.csdn.net/u014113117/article/details/51311837" target="_blank" rel="noopener">MAC、IP和路由传输封装过程</a></p>]]></content>
<categories>
<category> Network </category>
</categories>
<tags>
<tag> Forwarding </tag>
<tag> Routing </tag>
<tag> ARP </tag>
</tags>
</entry>
<entry>
<title>记一次完整的网络通信过程</title>
<link href="/2017/11/05/Network/Mechanism/communication/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><p>写完前面几篇对网络硬件设备以及对应工作机制的介绍之后,我觉得有必要再多有一篇博文对网络中的端到端通信过程进行完整的解析,本文对同一网段内、跨网段主机通信两种情形分别列举两个简单示例,分别描述了通信过程中各硬件设备和网络协议的协同工作流程。<br><a id="more"></a></p><h2 id="通用流程"><a href="#通用流程" class="headerlink" title="通用流程"></a>通用流程</h2><hr><p>网络设备之间的通信首先会经过如下两个步骤:</p><ol><li><p>发送主机应用程序生成数据,准备向外发送一个数据包;</p></li><li><p>发送主机(TCP/IP协议栈)判断这个数据包的目的地址是否在同一个网段:本机IP地址和目的IP地址分别与本机子网掩码作按位与操作,结果一致则在同一网段,否则两者分别位处不同网段,针对两种不同情况,通信流程也有明显差异。</p></li></ol><p><strong>注意</strong>:网关通常指的是路由器的一个物理端口,此时路由器称为“网关路由器”。路由器转发数据包时不会对它的源IP地址和目的IP地址做修改,只会修改MAC地址。</p><h2 id="网内通信"><a href="#网内通信" class="headerlink" title="网内通信"></a>网内通信</h2><hr><p>网内通信,即通信双方都位处同一网段中,数据传输无需经过路由器(或三层交换机),即可由本网段自主完成。</p><p>以下图拓扑为例:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/Mechanism/mac-arp-routing-table/example-topology-arp.png" alt="image"></p><p>在第一节的两个步骤基础上,若目的主机与发送主机属于同一网段,假设发送主机的ARP表中并无目的主机对应的表项,则发送主机会以目的主机IP地址为内容,广播ARP请求以期获知目的主机MAC地址,并通过交换机(除到达端口之外的所有端口发送,即洪泛(Flooding))向全网段主机转发,而只有目的主机接收到此ARP请求后会将自己的MAC地址和IP地址装入ARP应答后将其回复给发送主机,发送主机接收到此ARP应答后,从中提取目的主机的MAC地址,并在其ARP表中建立目的主机的对应表项(IP地址到MAC地址的映射),之后即可向目的主机发送数据,将待发送数据封装成帧,并通过二层设备(如交换机)转发至本网段内的目的主机,自此完成通信。</p><p>具体分析过程参见<a href="http://dongdongdong.me/2017/11/15/Network/Mechanism/mac-arp-routing-table"><strong>此篇</strong></a></p><h2 id="网际通信"><a href="#网际通信" class="headerlink" title="网际通信"></a>网际通信</h2><hr><p>网际通信,即通信双方分处不同网段,数据传输需经过路由器(或三层交换机)才能完成不同网段的通信。</p><p>以下图拓扑为例:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/Mechanism/mac-arp-routing-table/example-topology-router.png" alt="image"></p><p>在第一节的两个步骤基础上,若目的主机与发送主机不在同一网段,则需进行以下五个步骤:</p><ol><li><p>假设发送主机的路由表中也无目的主机所在网段对应的表项,发送主机会将此数据包先发送至默认网关,由默认网关完成转发,假设发送主机的ARP表中并无默认网关对应的表项,则进行上一节中相同的ARP过程以获知默认网关MAC地址,之后根据此MAC地址,本机将数据包封装成帧后发送给默认网关。值得注意的是,此数据包的目的MAC地址为默认网关的MAC地址,而目的IP地址仍为目的主机的IP地址,即:源、目的MAC地址在帧转发过程中会被修改为本跳和下一跳的MAC地址,而源、目的IP地址则始终不变;</p></li><li><p>假设两个通信网段仅通过此默认网关连接(两个网段之间只有默认网关这一跳),默认网关提取数据包的目的IP地址,并查找路由表对应表项,通过与每个表项的掩码(<strong>Genmask</strong>)进行按位与操作,得到的结果如果与对应目的网段(<strong>Destination</strong>)相同,在本例中将会找到一个满足要求的路由表项,并发现此表项指示的下一跳(<strong>Gateway</strong>)的IP地址为0.0.0.0,即说明目的主机属于与该网关的某个端口(由<strong>Iface</strong>指示)直连的网段,会向该端口转发此数据包,但在此之前,假设网关的ARP表项中并无目的主机对应的表项,那么它还是需要向目的网段广播ARP请求以获知目的主机的MAC地址,并随后以此MAC地址作为目的MAC地址,以路由表项<strong>Iface</strong>指示的端口的MAC地址为源MAC地址,将数据包封装成帧向目的主机发送。</p></li><li><p>假设两个通信网段之间包含不止一跳,那么默认网关进行步骤2类似过程,重新封装数据包转发到下一个网关;</p></li><li><p>直到网关发现目的网段与本机的某个端口直连,再行获取目的主机的MAC地址,并将数据包重新封装成帧发送到目的主机网卡;</p></li><li><p>目的主机协议栈验证目的IP地址是自身的IP地址后交付给上层应用,自此完成通信。</p></li></ol><p>具体分析过程参见<a href="http://dongdongdong.me/2017/11/15/Network/Mechanism/mac-arp-routing-table"><strong>此篇</strong></a></p>]]></content>
<categories>
<category> Network </category>
</categories>
<tags>
<tag> Communication </tag>
<tag> Forwarding </tag>
<tag> Routing </tag>
<tag> ARP </tag>
</tags>
</entry>
<entry>
<title>交换机与路由器详细比较</title>
<link href="/2017/11/05/Network/Hardware/switch-router/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><hr><p>作为计算机网络中最重要的两种数据包转发设备,交换机和路由器在功能设计方面既存在本质差别,又包含诸多相似之处,本文从两种设备的工作原理出发,详细介绍了它们之间的种种区别与联系。<br><a id="more"></a></p><h2 id="交换机-Switch"><a href="#交换机-Switch" class="headerlink" title="交换机(Switch)"></a>交换机(Switch)</h2><hr><h3 id="核心功能"><a href="#核心功能" class="headerlink" title="核心功能"></a>核心功能</h3><p>交换机的三个核心功能如下:</p><ol><li><strong>学习</strong>:以太网交换机了解与每一端口相连设备的MAC地址,并将地址同相应的端口映射对应起来存放在交换机缓存中的MAC地址转发表中;</li><li><strong>转发</strong>:或称“过滤”,当一个数据帧的目的MAC地址在MAC地址表中有映射条目时,它被转发到连接目的节点的端口而不是所有的端口;若没有对应条目,则转发至交换机除接收端口外的全部端口;</li><li><strong>消除环路</strong>:当检测到出现冗余回路时,以太网交换机通过生成树协议消除回路。</li></ol><h3 id="工作流"><a href="#工作流" class="headerlink" title="工作流"></a>工作流</h3><p>交换机通过以下步骤完成数据帧转发这一核心功能:</p><ol><li>交换机通过“自学习”过程:根据收到的数据帧中的源MAC地址及其到达端口建立起映射关系,并将其写入转发表中;</li><li>交换机将数据帧中的目的MAC地址同已缓存的转发表条目比较,以决定向哪个端口进行转发并从该端口将数据帧输出至链路;</li><li>如数据帧中的目的MAC地址不在转发表中,则向除其到达端口以外的所有端口转发,这一过程称为泛洪(Flood);</li><li>广播帧或组播帧则向所有端口转发。</li></ol><h3 id="工作原理"><a href="#工作原理" class="headerlink" title="工作原理"></a>工作原理</h3><ol><li>交换机的每一个端口所连接的网段都是一个独立的冲突域,即交换机隔离冲突域;</li><li>交换机所连接的设备仍然在同一个广播域内,也就是说,交换机不能隔离广播域(VLAN环境中除外);</li><li>交换机依据帧头信息进行转发,因此说交换机是工作在数据链路层的网络设备,此处交换机仅指传统的二层交换设备。后文还会介绍工作在三层甚至四层的交换机。</li></ol><h3 id="转发模式"><a href="#转发模式" class="headerlink" title="转发模式"></a>转发模式</h3><p>交换机有两种转发方式:</p><h4 id="直通转发-Cut-Through"><a href="#直通转发-Cut-Through" class="headerlink" title="直通转发(Cut-Through)"></a>直通转发(Cut-Through)</h4><p>交换机一旦解析到数据包的目的MAC地址,就开始查询MAC地址转发表,向目的端口转发该数据包。往往,交换机在接收到数据包的前6个字节时,实际已经知道目的地址,从而可以决定向哪个端口转发该数据包。直通转发技术的优点是转发速率快、减少延时与提升整体吞吐率。其缺点是交换机在没有完全接收并检查数据包的正确性之前就开始了数据转发,这实际上会给整个交换网络带来许多垃圾通讯包,交换机会被误解为发生了广播风暴。</p><p>因此,直通转发技术适用于网络链路质量较好、出错数据包较少的网络环境。</p><h4 id="存储转发-Store-and-Forward"><a href="#存储转发-Store-and-Forward" class="headerlink" title="存储转发(Store-and-Forward)"></a>存储转发(Store-and-Forward)</h4><p>存储转发技术要求交换机在接收到全部数据包后再决定如何转发,这样一来,由于在转发之前已经收到了数据帧的全部字段内容,当然也收到了最后一个字段FCS(Frame Check Sequence,帧校验序列),因此,交换机可以在转发之前通过重新计算FSC与接收到的FCS比较从而检查数据包的完整性和正确性。存储转发技术的优点是没有残缺数据包转发,减少了潜在的不必要数据转发。其缺点是由于引入了校验逻辑,增加了额外的时间开销,因而转发速率比直通转发技术要慢。</p><p>因此,存储转发技术适用于普通链路质量的网络环境。</p><h3 id="三层交换机"><a href="#三层交换机" class="headerlink" title="三层交换机"></a>三层交换机</h3><p>三层交换机就是具有部分路由器功能的交换机,三层交换机的最重要目的是加快大型局域网内部的数据交换,所具有的路由功能也是为这目的服务的,能够做到<strong>一次路由,多次转发</strong>。对于数据包转发等规律性的过程由<strong>硬件</strong>高速实现,而像路由信息更新、路由表维护、路由计算、路由确定等功能,由<strong>软件</strong>实现。</p><p><strong>三层交换技术 = 二层交换技术+三层转发技术</strong>:传统交换技术是在OSI网络模型第二层,即数据链路层进行操作的,而三层交换技术是在网络模型中的第三层实现了数据包的高速转发,既可实现网络路由功能,又可根据不同网络状况做到最优网络性能。</p><p>三层交换技术实际就是将路由技术与交换技术合二为一的技术,在对第一个数据流进行路由后,它将会产生一个MAC地址与IP地址的映射表,当同样的数据流再次通过时,将根据此表直接从二层通过而不是再次路由,从而消除了由网络层进行路由选择而造成的转发延迟,提高了数据包的转发效率。</p><p>举例说明如下:</p><blockquote><p>假设两个使用IP协议的站点A、B通过第三层交换机进行通信。发送站点A在开始发送时,把自己的IP地址与B站的IP地址比较,利用子网掩码判断B站是否与自己在同一子网内。</p><p>若两个站点在同一子网内,则进行二层的转发,如一开始转发数据所需的目的MAC地址,A站就发送一个ARP广播请求,B站接收到后会将其MAC地址封装在ARP应答中发送给A,A利用此目的MAC地址封装数据包并发送给交换机,交换机此时启用二层交换模块,查找MAC地址转发表,将数据包转发至对应端口并最终发送给站点B;</p><p>若两个站点不在同一子网,那么A想要实现与B的通讯,在ARP缓存表中没有对应的MAC地址条目,就将第一个数据包发送给一个缺省网关,该网关一般在操作系统中已设定好,实际就是三层交换机的第三层交换模块。然后数据包被发送给缺省网关,目的MAC地址就是缺省网关与A相连的端口的MAC地址。三层交换机接收到此数据包,查询路由表以确定到达B的路由(下一跳IP地址和输出端口),发现主机B在与其端口直连的网段内,于是三层交换机重新封装该数据包,构造一个新的帧头,其中以交换机的输出端口MAC地址为源MAC地址,查询ARP表以获得主机B的MAC地址,以此作为目的MAC地址。通过一定的识别触发机制,确立主机A与B的MAC地址及转发端口的对应关系,实际就是交换机的二层交换模块的“自学习”功能,并记录到转发表,此后从A与B的数据包通信,就直接交由二层交换模块完成转发,这就是所说的一次路由多次转发。</p></blockquote><p>由于仅仅在路由过程中才需要三层处理,绝大部分数据都通过二层交换转发,因此三层交换机的速度很快,接近二层交换机的速度,同时比相同路由器的价格低很多。</p><p>在实际应用过程中,典型的做法是:处于同一个局域网中的各个子网的互联以及局域网中VLAN间的路由,用三层交换机来代替路由器,而只有局域网与公网互联之间要实现跨地域的网络访问时,才通过专业路由器。</p><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>传统交换机通常指工作在数据链路层的交换设备,采用“自学习”方式维护与MAC地址与其输出端口的映射关系,并在数据链路层根据数据帧的目的MAC地址完成其转发功能。二层交换机一般都有专门用于处理数据包转发的ASIC(Application Specific Integrated Circuit,特定用途集成电路),因此转发速度可以做到非常快,不同厂家采用的ASIC不同,直接影响产品性能。</p><p>三层交换,也称多层交换技术,或IP交换技术,是相对于传统交换概念而提出的:传统的交换技术是在OSI网络标准模型中的第二层,即数据链路层进行操作的,而三层交换技术是在网络模型中的第三层,即网络层实现了数据包的高速转发。简单地说,三层交换技术就是:二层交换技术+三层转发技术。三层交换技术的出现,解决了局域网中网段划分之后,网段中子网必须依赖路由器进行管理的局面,解决了传统路由器低速、复杂所造成的网络瓶颈问题。</p><h2 id="路由器-Router"><a href="#路由器-Router" class="headerlink" title="路由器(Router)"></a>路由器(Router)</h2><hr><h3 id="核心功能-1"><a href="#核心功能-1" class="headerlink" title="核心功能"></a>核心功能</h3><p>一般而言,路由器工作在网络层,其工作模式与二层交换类似,但路由器工作在第三层,这个区别决定了路由器与交换机在转发数据包时使用的控制信息(首部字段)是不同的。</p><p>路由器内部有一个路由表,这表标明了如果要去某个地方,下一步应该往哪走。路由器从某个端口收到一个数据包,它首先把链路层的包头去掉(拆包),读取目的IP地址,然后查找路由表,若能确定下一步往哪送,则再加上链路层的包头(打包),把该数据包转发出去;如果不能确定下一步的地址,则向源地址返回一个信息(Unreachable,目的地不可达),并把这个数据包丢弃。</p><h3 id="工作流-1"><a href="#工作流-1" class="headerlink" title="工作流"></a>工作流</h3><p>路由器接收到数据包后,首先在其自身维护的路由表中查找它的目的地址,若找到了目的地址对应项即获知转发的下一跳地址,就在数据包的MAC首部中添加该IP地址对应的MAC地址作为目的MAC地址,同时IP首部中的TTL(Time to Live)字段也开始减数,并重新计算校验和,最终在其输出端口完成数据帧的重新封装,源MAC地址为输出端口的MAC地址。</p><p>路由器在工作时会运行某种路由通信协议生成路由表,用于生成并维护在数据包到来时查找匹配目的IP地址的表项。如果到某个特定节点有一条以上的路径,则基于预先确定的路由准则是选择最优(代价最小)的传输路径。由于各网段及其相互链接的情况可能会因环境变化而变化,因此路由信息一般也按所使用的路由通信协议的规定根据实时网络拓扑情况而即时更新。</p><h3 id="小结-1"><a href="#小结-1" class="headerlink" title="小结"></a>小结</h3><p>路由技术其实是由两项最基本的活动组成,即<strong>决定最优路径</strong>和<strong>传输数据包</strong>。其中,数据包的传输相对较为简单和直接,而路由的确定则更加复杂一些。路由算法在路由表中写入各种不同的信息,路由器会根据数据包所要到达的目的地选择最佳路径把数据包发送到可以到达该目的地的下一台路由器处。当下一台路由器接收到该数据包时,也会查看其目标地址,并使用合适的路径继续传送给后面的路由器。依次类推,直到数据包到达最终目的地。 </p><h2 id="冲突域-VS-广播域"><a href="#冲突域-VS-广播域" class="headerlink" title="冲突域 VS 广播域"></a>冲突域 VS 广播域</h2><hr><h3 id="冲突域-Collision-Domain"><a href="#冲突域-Collision-Domain" class="headerlink" title="冲突域(Collision Domain)"></a>冲突域(Collision Domain)</h3><p>以太网使用CSMA/CD(Carrier Sense Multiple Access with Collision Detection,带碰撞检测的载波多路侦听)算法来进行通信介质的访问控制。如果两个或者更多站点同时检测到信道空闲而有帧准备发送,它们将发生冲突,一组竞争信道访问的站点称为冲突域。</p><p>简单来说,在以太网中,如果某个CSMA/CD网络上的任意两个节点在同时通信,即发送数据时会发送冲突,它们几乎同时检测到冲突且只好都放弃传输,那么这个CSMA/CD网络内的全部节点就构成了一个冲突域。如果以太网中的各个网段内部都是以集线器连接,因为集线器会把接收到的帧转发至自身的全部端口,还是会引起冲突,所以即使包含集线器,该网段仍然是一个冲突域。</p><p>显然,同一个冲突域中的节点竞争信道,就会导致冲突和退避。而不同冲突域的节点不会竞争公共信道,则不会发生冲突。在交换式局域网中,每个交换机端口就对应一个冲突域,端口就是冲突域终点。由于交换机具有交换功能,在不同端口之间都有专门的信道,故不同端口的节点之间不会产生冲突。如果每个端口只连接一个节点,那么在任何一对节点之间都不会发生冲突。若一个端口连接到一个共享式局域网(内部多台计算机相连,或直连,或与集线器相连等等方式构成局域网),那么在与该端口直连局域网中的任意节点之间都会产生冲突,但与该端口直连的节点和与交换机其它端口相连的节点之间通信则不会发生冲突,这样交换机就隔离了不同的冲突域,使得冲突只发生在各个冲突域内部,而不会影响其它冲突域。</p><h4 id="集线器"><a href="#集线器" class="headerlink" title="集线器"></a>集线器</h4><p>集线器是一种物理层设备,本身不能识别MAC地址和IP地址,当集线器连接的主机设备之间传输数据时,数据包是以广播方式进行传播,即集线器会将收到的数据包广播至其所有端口,而由每台主机根据数据包与自身MAC地址是否匹配来决定是否接收。</p><p>这种情况下,同一时刻由集线器连接的网络中只能有一个节点在占用信道传输数据,即所谓的”Top Talker“,其它节点在检测到信道忙碌则退避等待,若信道空闲时同时有两个或多个节点开始传输,则会发生冲突,各自放弃数据传输。集线器所有端口共享集线器的整个带宽,即所有端口为一个冲突域,如下图所示:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/Hardware/switch-router/collision-domain-hub.png" alt="image"></p><h4 id="交换机"><a href="#交换机" class="headerlink" title="交换机"></a>交换机</h4><p>交换机工作在数据链路层,在接收数据帧后,通过查找自身维护的MAC地址转发表找出对应输出端口,并把数据传送到目的端口。</p><p>交换机在同一时刻可进行多个端口之间的数据传输,即支持“并发传输”,这是由于交换机不同端口之间都有专门的链路相连,且独享全部带宽。每一端口相连的都是独立的物理网段,连接在端口上的网络设备独自享有全部带宽,因此,交换机起到了分割不同冲突域的作用,每一个端口相连的网段为一个独立的冲突域,如下图所示:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/Hardware/switch-router/collision-domain-switch.png" alt="image"></p><h4 id="小结-2"><a href="#小结-2" class="headerlink" title="小结"></a>小结</h4><p>集线器是一个标准的共享式设备,也就是同一时刻只有一个端口下联的设备可以发送数据。正常工作时,集线器随机选出某一端口设备并让它独占全部带宽与集线器上联设备(如交换机、路由器等)进行通信。因此,集线器设备的所有端口即形成了一个冲突域。</p><p>为了有效避免冲突,局域网中使用交换机(Switch)来分割冲突域。对网络进行分割的原因是为了分离流量并创建更小的冲突域来使用户获得更高的带宽,否则同一时刻数据太多容易导致网络拥挤形成阻塞。</p><h3 id="广播域-Broadcast-Domain"><a href="#广播域-Broadcast-Domain" class="headerlink" title="广播域(Broadcast Domain)"></a>广播域(Broadcast Domain)</h3><p>常见广播情形有如下两种:</p><ol><li>源节点发送数据帧给全部主机,在封装数据帧时直接将广播地址:ff:ff:ff:ff:ff:ff作为目的MAC地址,则交换机在接收该帧时解析到该广播地址,就会复制数据帧并向全部非接收端口转发;</li><li>当转发表中没有匹配的目的MAC地址项时,交换机会将数据帧复制并转发至全部非接收端口,这一过程实际也是广播。</li></ol><h4 id="路由器"><a href="#路由器" class="headerlink" title="路由器"></a>路由器</h4><p>然而,以上两种形式的广播会到什么地方结束呢?答案是路由器。<br>为什么到路由器就结束了呢?再来回顾一下路由器的工作流程:</p><p>当路由器接收到一个数据包时,首先提取数据包头的目的MAC信息,与自身MAC表比较,分两种情况:</p><ol><li>如果找到对应项,则按MAC表进行转发(与交换机一样);</li><li>如果没找到则提取数据包头的目的IP地址,与自身路由表进行比较,这里又分两种情况:<br> (1) 存在对应的路由表项,则按路由表转发(与查到MAC表很像);<br> (2) 没找到对应路由表项,则按缺省路由转发至默认网关(自始自终没有出现过广播!)。</li></ol><p>路由表和MAC的区别在于,路由表存放的是目的IP,即下一步要去地方的IP地址。</p><p>广播在路由器能够结束,原因就在于路由器会在查找不到对应MAC表时,根据目的IP进行路由。路由过程的两种情况都不存在广播。因此,路由器可以隔离广播域,如下图所示:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/Hardware/switch-router/broadcast-domain-router.png" alt="image"></p><h4 id="小结-3"><a href="#小结-3" class="headerlink" title="小结"></a>小结</h4><p>以太网中,冲突域通常是由集线器组织的,与同一个集线器相连的全部节点就组成了一个冲突域。交换机的每个端口相连的网络都是一个单独的冲突域。</p><p>广播域是指一个节点发出一个广播信号后,能够接收到该信号的全部节点的集合。通常来说一个局域网就是一个广播域。交换机的所有端口都在同一个广播域内。</p><ul><li><strong>冲突域</strong>:在同一个冲突域中的每一个节点都能收到它们之中任意一个节点发送的帧;</li><li><strong>广播域</strong>:网络中能接收到任意节点发出的广播帧的所有节点的集合。</li></ul><p>冲突域是基于第一层,即物理层的;广播域是基于第二层,即数据链路层的。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><hr><p>二层交换机工作在数据链路层,路由器工作在网络层,而三层交换机则工作在数据链路层和网络层。</p><h3 id="二层交换机-VS-三层交换机"><a href="#二层交换机-VS-三层交换机" class="headerlink" title="二层交换机 VS 三层交换机"></a>二层交换机 VS 三层交换机</h3><p>三层交换机使用了三层交换技术,简单地说,三层交换技术就是:二层交换技术+三层转发技术。即三层交换机在包含了全部二层交换机的功能之外,还具备一部分的网络层功能。它解决了局域网中网段划分之后,网段中子网必须依赖路由器进行管理的局面,解决了传统路由器低速、复杂所造成的网络瓶颈问题。</p><h3 id="二层交换机-VS-路由器"><a href="#二层交换机-VS-路由器" class="headerlink" title="二层交换机 VS 路由器"></a>二层交换机 VS 路由器</h3><p>二层交换机即传统交换机,从网桥发展而来,属于OSI第二层即数据链路层设备。它根据MAC地址寻址,转发表(目的MAC地址到输出端口的映射)的建立和维护由交换机自动进行。</p><p>路由器属于OSI第三层即网络层设备,它根据IP地址进行寻址,通过运行路由协议生成和维护路由表(目的IP地址到输出端口、下一跳IP地址的映射)。</p><p>交换机最大的好处是快速,由于交换机只须识别以太网帧首部中的MAC地址,直接根据MAC地址产生选择转发端口,算法简单,便于ASIC实现,因此转发速度极高。但与此同时,交换机的工作机制也带来一些问题:</p><ol><li><p><strong>回路</strong>:根据交换机地址学习和转发表建立算法,交换机之间不允许存在回路。一旦存在回路,必须启动生成树算法,阻塞掉产生回路的端口。而路由器的路由协议没有这个问题,路由器之间可以有多条通路来平衡负载,提高可靠性;</p></li><li><p><strong>负载集中</strong>:交换机之间只能有一条通路,使得信息集中在一条通信链路上,不能进行动态分配,以平衡负载。而路由器的路由协议算法可以避免这一点,OSPF路由协议算法不但能产生多条路由,而且能为不同的网络应用选择各自不同的最佳路由;</p></li><li><p><strong>广播控制</strong>:交换机只能缩小冲突域,而不能缩小广播域。整个交换式网络就是一个大的广播域,广播报文散布到整个交换式网络。而路由器可以隔离广播域,广播报文不能通过路由器继续进行广播;</p></li><li><p><strong>子网划分</strong>:交换机只能识别MAC地址。MAC地址是物理地址,而且采用<strong>平坦</strong>的地址结构,因此不能根据MAC地址来划分子网。而路由器识别IP地址,IP地址由网络管理员分配,是逻辑地址且IP地址具有<strong>层次</strong>结构,被划分成网络号和主机号,可以非常方便地用于划分子网,路由器的主要功能就是用于连接不同的网络,甚至是异构网络;</p></li><li><p><strong>保密问题</strong>:虽说交换机也可以根据帧的源MAC地址、目的MAC地址和其他帧中内容对帧实施过滤,但路由器根据报文的源IP地址、目的IP地址、TCP端口地址等内容对报文实施过滤,更加直观方便;</p></li><li><p><strong>介质相关</strong>:交换机作为桥接设备也能完成不同链路层和物理层之间的转换,但这种转换过程比较复杂,不适合ASIC实现,势必降低交换机的转发速度。因此目前交换机主要完成相同或相似物理介质和链路协议的网络互连,而不会用来在物理介质和链路层协议相差甚远的网络之间进行互连。而路由器则不同,它主要用于不同网络之间互连,因此能连接不同物理介质、链路层协议和网络层协议的网络。路由器在功能上虽然占据了优势,但价格昂贵,报文转发速度低。近几年,交换机为提高性能做了许多改进,其中最突出的改进是虚拟网络和三层交换。</p></li></ol><p>路由器可以划分子网,从而缩小广播域,减少广播风暴对网络的影响。路由器每一端口连接一个子网,广播报文不能经过路由器广播出去,连接在路由器不同端口的子网属于不同子网,子网范围由路由器物理划分。</p><p>对交换机而言,每一个端口对应一个网段,而子网由若干网段构成,通过对交换机端口的组合,可以逻辑划分子网。广播报文只能在子网内广播,不能扩散到别的子网内,因此可通过合理划分逻辑子网,达到控制广播的目的。由于逻辑子网由交换机端口任意组合,没有物理上的相关性,因此称为虚拟子网,或叫虚拟网(VLAN)。虚拟网技术不用路由器就解决了广播报文的隔离问题,且虚拟网内网段与其物理位置无关,即相邻网段可以属于不同虚拟网,而相隔甚远的两个网段可能属于不同虚拟网,也可能属于同一个虚拟网。不同虚拟网内的终端之间不能相互通信,增强了对网络内数据的访问控制。</p><h3 id="三层交换机-VS-路由器"><a href="#三层交换机-VS-路由器" class="headerlink" title="三层交换机 VS 路由器"></a>三层交换机 VS 路由器</h3><p>在第三层交换技术出现之前,几乎没有必要将路由功能器件和路由器区别开来,他们完全是相同的:提供路由功能并由路由器负责执行。然而,现在第三层交换机完全能够执行传统路由器的大多数功能。作为一种可跨层次工作的网络互连设备,第三层交换机具有以下特征:</p><ol><li>转发基于第三层地址的业务流;</li><li>完全交换功能;</li><li>可以完成特殊服务,如报文过滤或认证;</li><li>执行或不执行路由处理。</li></ol><p>第三层交换机与传统路由器相比有如下优点:</p><ol><li>子网间传输带宽可任意分配:传统路由器每个接口连接一个子网,子网通过路由器进行传输的速率被接口的带宽所限制。而三层交换机则不同,它可以把多个端口定义成一个虚拟网,把多个端口组成的虚拟网作为虚拟网接口,该虚拟网内信息可通过组成虚拟网的端口送给三层交换机,由于端口数可任意指定,子网间传输带宽没有限制。</li><li>合理配置信息资源:由于访问子网内资源速率和访问全局网中资源速率没有区别,子网设置单独服务器的意义不大,通过在全局网中设置服务器群不仅节省费用,更可以合理配置信息资源。</li><li>降低成本:通常的网络设计用交换机构成子网,用路由器进行子网间互连。目前采用三层交换机进行网络设计,既可以进行任意虚拟子网划分,又可以通过交换机三层路由功能完成子网间通信,为此节省了使用路由器带来的高昂成本。</li><li>交换机之间连接灵活:作为交换机,它们之间不允许存在回路,作为路由器,又可有多条通路来提高可靠性、平衡负载。三层交换机用生成树算法阻塞造成回路的端口,但进行路由选择时,依然把阻塞掉的通路作为可选路径参与路由选择。</li></ol><p>交换机和路由器是性能和功能的矛盾体,交换机交换速度快,但控制功能弱,路由器控制性能强,但报文转发速度慢。解决这个矛盾的最新技术是三层交换,既有交换机线速转发报文能力,又有路由器良好的控制功能。</p><h2 id="参考资源"><a href="#参考资源" class="headerlink" title="参考资源"></a>参考资源</h2><hr><p>[1] <a href="http://blog.sina.com.cn/s/blog_5e1a889d0101370d.html" target="_blank" rel="noopener">交换机的工作原理</a><br>[2] <a href="http://blog.csdn.net/shmily_cml0603/article/details/9334795" target="_blank" rel="noopener">二、三层交换机和路由器的工作原理与主要区别</a><br>[3] <a href="http://www.net130.com/ccnp/20040229009.htm" target="_blank" rel="noopener">三层交换机与路由器的比较</a><br>[4] <a href="http://blog.smallmuou.xyz/network/2017/04/14/%E6%95%99%E4%BD%A0%E8%AF%BB%E6%87%82%E8%B7%AF%E7%94%B1%E8%A1%A8.html" target="_blank" rel="noopener">教你读懂路由表</a><br>[5] <a href="http://net.zhiding.cn/network_security_zone/2008/0514/860886.shtml" target="_blank" rel="noopener">网络知识:二层、三层、四层交换机的区别</a></p>]]></content>
<categories>
<category> Network </category>
</categories>
<tags>
<tag> Switch </tag>
<tag> Router </tag>
</tags>
</entry>
<entry>
<title>Git工作区、版本库与暂存区</title>
<link href="/2017/10/31/VC/Git/working_directory-repository-stage/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><p>Git与其它版本控制系统如SVN的一个最大的不同之处就是发明了暂存区的概念,本文从创建Git版本库开始,依次描述了追踪文件、修改文件、丢弃修改、提交修改等基本Git操作,在此过程中介绍了Git版本控制系统中三个重要概念:工作区、版本库与暂存区。</p><a id="more"></a><h2 id="工作区-Working-Directory"><a href="#工作区-Working-Directory" class="headerlink" title="工作区(Working Directory)"></a>工作区(Working Directory)</h2><hr><p>就是你能在电脑中看到的目录,即本地目录,比如我的<strong>GitTest</strong>就是一个工作区,准确的来说,Git工作区应该指的是执行过git init初始化后的本地目录,下图可以看到现在工作区内只有一个文件:a.txt:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/VC/Git/working_directory-repository-stage/0-git-init.png" alt="image"></p><h2 id="版本库-Repository"><a href="#版本库-Repository" class="headerlink" title="版本库(Repository)"></a>版本库(Repository)</h2><hr><p>工作区有一个隐藏目录<strong>.git</strong>,这个不是工作区,而是Git的版本库。<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/VC/Git/working_directory-repository-stage/repository.png" alt="image"></p><p>Git版本库里存了很多东西,其中最重要的就是称为”Stage”的暂存区,还有Git为我们自动创建的第一个本地分支Master。</p><p>分支与HEAD的概念之后的博文会详细讲解。</p><h2 id="暂存区-Stage"><a href="#暂存区-Stage" class="headerlink" title="暂存区(Stage)"></a>暂存区(Stage)</h2><hr><p>前面说到暂存区实际是Git版本库里面的一个区域,具体的结构参见下图:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/VC/Git/working_directory-repository-stage/working_directory-repository-stage.png" alt="image"></p><p>下文中会结合一次完整的版本控制操作过程来具体讲解各个区域的作用。</p><h2 id="本地版本控制流程实例"><a href="#本地版本控制流程实例" class="headerlink" title="本地版本控制流程实例"></a>本地版本控制流程实例</h2><hr><p>实例开始之前在强调一下,使用git命令时,status绝对是一个功能超乎想象重要的参数,让使用者可以获得整个版本文件的实时视图。</p><p>流程按照上图中显示的git版本库继续操作,所有操作均在本地分支Master上完成:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/VC/Git/working_directory-repository-stage/1-untracked-file.png" alt="image"></p><p>工作区中包含未被追踪的文件(Untracked files):<strong>a.txt</strong>,按提示使用命令:<strong>git add <文件名></strong>,就可以将该文件添加至<strong>暂存区</strong>,并使用命令<strong>git status</strong>,查看当前版本完整视图:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/VC/Git/working_directory-repository-stage/2-git-add.png" alt="image"></p><p>根据提示使用命令:<strong>git rm - -cached <文件名></strong>,可取消缓存在暂存区的文件修改:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/VC/Git/working_directory-repository-stage/3-git-rm-cached.png" alt="image"></p><p>发现暂存区的文件被弹出,<strong>a.txt</strong>重新回到了<strong>Untracked files</strong>下,再次执行<strong>git add</strong>将其加入暂存区:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/VC/Git/working_directory-repository-stage/4-git-add-again.png" alt="image"></p><p>接下来使用命令:<strong>git commit -m “message”</strong>,将暂存区文件提交,此时终端显示“nothing to commit, working directory clean”(没有可提交的内容,工作区是干净的),这是因为工作区中的文件修改已全部提交至版本库中的本地分支Master上,工作区与Master分支内容完全一致,即没有“脏”内容(类比数据库的“脏读”(Dirty Read),脏读是指一个事务读到了另一个事务还未提交的数据,实际就是存在有未提交的数据)的情况:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/VC/Git/working_directory-repository-stage/5-git-commit.png" alt="image"></p><p>下面我们试着在工作区创建一个新文件<strong>b.txt</strong>,此时显然该文件属于<strong>Untracked files</strong>:</p><pre><code>b.txt这是第一次修改.</code></pre><p><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/VC/Git/working_directory-repository-stage/6-git-touch-new-file.png" alt="image"><br>同样将其添加到暂存区中:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/VC/Git/working_directory-repository-stage/7-git-add.png" alt="image"></p><p>如果你足够细心,不难发现一些<strong>有意思的事情</strong>:同样是追踪文件即将文件修改加入暂存区,两次执行<strong>git add</strong>命令的效果一样,但是Git给出的取消文件暂存命令提示不太一样:</p><ul><li><p>将<strong>a.txt</strong>加入暂存区时,Git提示取消暂存的命令为:<strong>git rm - -cached <文件名></strong>。</p></li><li><p>将<strong>b.txt</strong>加入暂存区时,Git提示取消暂存的命令为:<strong>git reset HEAD <文件名></strong>。</p></li></ul><p>出现这种差异是因为<strong>a.txt</strong>属于第一次提交(Initial Commit)的文件内容,对应取消暂存的命令就是:<strong>git rm - -cached <文件名></strong>。而如果在初始化版本库(即<strong>git init</strong>)之后新增的文件,取消暂存的命令则是:<strong>git reset HEAD <文件名></strong>。</p><p>那我们执行取消暂存<strong>b.txt</strong>的命令:<strong>git reset HEAD b.txt</strong>,发现<strong>b.txt</strong>重新回到<strong>Untracked files</strong>类别下:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/VC/Git/working_directory-repository-stage/8-git-reset-HEAD-unstage.png" alt="image"></p><p>再次将<strong>b.txt</strong>添加到暂存区:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/VC/Git/working_directory-repository-stage/9-git-add.png" alt="image"></p><p>然后我们在最后提交之前再对<strong>b.txt</strong>进行修改,增加一行内容:</p><pre><code>b.txt这是第一次修改.这是第二次修改.</code></pre><p><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/VC/Git/working_directory-repository-stage/10-git-modify.png" alt="image"></p><p>发现<strong>b.txt</strong>同时在暂存区(Changes to be committed)和未暂存区(Changes not staged for commit,为了与暂存区对比,姑且这么称呼,实际上就是工作区,只不过对应文件在上一次被暂存后又再次被修改过,而修改过的文件内容还未被暂存),但是描述有些许不同:</p><ul><li><p>在暂存区中,<strong>b.txt</strong>前面修饰词为<strong>new</strong>,表示对于暂存区而言,这是<strong>b.txt</strong>第一次被加入到暂存区中。</p></li><li><p>在未暂存区中,<strong>b.txt</strong>前面修饰词为<strong>modified</strong>,表示对于为暂存区而言,它已经知道<strong>b.txt</strong>之前已经被暂存过一次,而在那之后又被修改过。</p></li></ul><p>对于状态下的<strong>b.txt</strong>文件,Git都给出了明确的提示,我们挨个尝试一遍。</p><p>直接取消上一次暂存:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/VC/Git/working_directory-repository-stage/11-git-reset-HEAD-unstage.png" alt="image"></p><p>再次暂存<strong>b.txt</strong>:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/VC/Git/working_directory-repository-stage/12-git-add.png" alt="image"></p><p>再次修改<strong>b.txt</strong>,增加一个文本行:</p><pre><code>b.txt这是第一次修改.这是第二次修改.这是第三次修改.</code></pre><p><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/VC/Git/working_directory-repository-stage/13-git-modify.png" alt="image"></p><p>执行命令:<strong>git checkout - - <文件名></strong>,该命令效果应该是丢弃本地修改:</p><p><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/VC/Git/working_directory-repository-stage/14-git-checkout-discard-changes.png" alt="image"></p><p><strong>cat</strong>命令查看<strong>b.txt</strong>内容发现上次对<strong>b.txt</strong>的修改(新增一个文本行)果然被丢弃。</p><p>再次修改<strong>b.txt</strong>内容,增加一个空行和一个文本行:</p><pre><code>b.txt这是第一次修改.这是第二次修改.这是第四次修改.</code></pre><p><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/VC/Git/working_directory-repository-stage/15-git-modify.png" alt="image"></p><p>这一次我们直接将修改后的<strong>b.txt</strong>添加至暂存区:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/VC/Git/working_directory-repository-stage/16-git-add.png" alt="image"></p><p>提交暂存区到本地分支Master:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/VC/Git/working_directory-repository-stage/17-git-commit.png" alt="image"></p><p>以上,我们就完成了一次本地分支的版本控制。</p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><hr><ol><li><strong>工作区</strong>:本地目录,包含所有被Git追踪的文件的实时内容;</li><li><strong>版本库</strong>:工作区中的.git文件夹,实际包含暂存区和当前分支指针HEAD,通过命令:<strong>git init</strong>可以初始化创建版本库;</li><li><strong>暂存区</strong>:暂存被追踪的文件修改,为正式提交到本地分支提供内容。</li></ol><ul><li><strong>工作区</strong>文件修改后通过命令:<strong>git add <文件名></strong>,将文件添加至<strong>暂存区</strong>。</li><li><strong>暂存区</strong>通过命令:<strong>git rm –cached <文件名></strong>(针对在初始化版本库之前就已经在<strong>工作区</strong>中的文件),或<strong>git reset HEAD <文件名></strong>(针对在初始化版本库之后才新增的文件),将暂存文件弹回到<strong>工作区</strong>。</li><li>若要在<strong>工作区</strong>丢弃文件自上一次暂存以来的全部修改,可执行命令:<strong>git checkout – <文件名></strong>。</li></ul><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><hr><p>[1] <a href="https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000/0013745374151782eb658c5a5ca454eaa451661275886c6000" target="_blank" rel="noopener">廖雪峰Git教程</a></p>]]></content>
<categories>
<category> Version Control </category>
</categories>
<tags>
<tag> Git </tag>
<tag> Working Directory </tag>
<tag> Repository </tag>
<tag> Stage </tag>
</tags>
</entry>
<entry>
<title>网络互连设备小结</title>
<link href="/2017/10/30/Network/Hardware/relay/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><hr><p>计算机网络往往由多种不同类型的网络通过特殊的设备相互连接而成,本文简要介绍了转发器、集线器、网桥、桥接器、交换机、路由器等多种网络互连设备的功能原理。</p><a id="more"></a><h2 id="网络互连设备"><a href="#网络互连设备" class="headerlink" title="网络互连设备"></a>网络互连设备</h2><hr><p>计算机网络往往由多种不同类型的网络互连(Interconnect)连接而成。如果几个计算机网络只是物理上连接在一起,它们之间并不能进行通信,那么这种形式上的“互连”毫无意义。因此在描述这些网络“互连”的同时,实际暗示这些相互连接的计算机是可以以某种方式进行通信的,由约定共同遵守的网络协议决定通信的方式和细节。换句话说,从逻辑和功能上来讲,这些计算机已经组成了一个大型的计算机网络,或称为“互联网络”(internet,注意首字母小写,Internet已经用来形容全球范围内最大的互联网络)。</p><p>将网络互相连接起来要使用一些中间设备(中间系统),ISO中术语称之为“中继”(Relay)系统,根据中继系统所在的层次,大致由如下五种中继系统:</p><ul><li>物理层(第一层)中继系统,即<strong>转发器(Repeater)</strong></li><li>数据链路层(第二层)中继系统,即<strong>网桥</strong>或<strong>桥接器(Bridge)</strong></li><li>网络层(第三层)中继系统,即<strong>路由器(Router)</strong></li><li>网桥和路由器混合物:<strong>桥路器(Brouter)</strong>,兼有网桥和路由器的功能</li><li>网络层以上的中继系统,即<strong>网关(Gateway)</strong></li></ul><p>当中继系统是转发器时,一般不称之为“网络互连”,因为这仅仅是把一个网络从物理上扩展了,本身仍只是一个网络。高层网关由于比较复杂,目前使用较少。因此通常在讨论网络互连时都是指利用交换机和路由器进行互连的网络。下面会简要介绍各种网络互连设备的基本功能和原理。</p><h2 id="转发器"><a href="#转发器" class="headerlink" title="转发器"></a>转发器</h2><hr><p>转发器(Repeater)又被称为中继器、放大器,执行物理层协议,负责第一层即物理层的数据中继。由于电信号在传输过程中会不断衰减,为了不让信号到达通信目的地时强度太小甚至完全消失,必须每传输一段距离就使用中继器放大电信号,使其能够传输到偏远的通信对端。</p><p>转发器用于互连两个相同类型的网段,主要功能是延伸网段和改变传输媒体,从而实现信息位的转发,它本身并不执行信号的过滤功能。</p><h2 id="集线器"><a href="#集线器" class="headerlink" title="集线器"></a>集线器</h2><hr><h3 id="基本原理"><a href="#基本原理" class="headerlink" title="基本原理"></a>基本原理</h3><p>集线器(HUB)是一种典型的特殊的转发器,它的作用简单来说就是将一些机器连接起来组成一个局域网。HUB本身是“中心”的意思,多用于建立星型或树型网络拓扑。它不具有智能处理能力,处理的数据只是电流而已,采用共享带宽的工作方式,连接到同一个集线器的所有计算机采用CSMA/CD方式竞争带宽。</p><p>集线器处于纯硬件网络底层设备,基本上不具有类似于交换机的“智能记忆”和“自学习”能力,更不具有交换机维护的MAC地址端口映射表(转发表,Forwarding Table),所以它发送数据时都是没有目的性的,直接采用广播方式发送。也就是说当它要向某节点发送数据时,不是直接把数据发送到目的节点,而是把数据包发送到所有与集线器物理相连的所有节点,因此它也可以叫做多端口转发器(Multiport Repeater)。</p><h3 id="核心功能"><a href="#核心功能" class="headerlink" title="核心功能"></a>核心功能</h3><p>集线器的主要功能就是对接收到的信号进行再生、整形和放大,以扩大网络信号的传输距离,同时把所有节点集中在以它为中心的节点上。它同样工作于物理层,与网卡(网络适配器)、网线等传输介质一样,属于局域网中的基础设备,采用CSMA/CD介质访问控制协议,即具有碰撞检测功能,有碰撞发生则需要避让,一个说完了,另一个人再说,通信效率较低。</p><h3 id="不足"><a href="#不足" class="headerlink" title="不足"></a>不足</h3><p>集线器的这种广播数据发送方式很明显有四点不足:</p><ol><li>用户数据包向所有节点发送,数据通信的安全性无法保证;</li><li>由于所有数据包都是向所有节点同时发送,加上其共享带宽的方式,带宽资源浪费严重,容易造成网络拥塞,降低整体网络性能;</li><li>非双工(半双工)传输,任意节点要么只发不收,要么只收不发;</li><li>连接到同一个集线器不同端口的多个主机,任意时刻只能有一个在发送数据,其余会由于检测到碰撞(信道竞争)而阻塞发送。</li></ol><h3 id="应用趋势"><a href="#应用趋势" class="headerlink" title="应用趋势"></a>应用趋势</h3><p>由于集线器会把收到的任何数字信号,经过再生和放大,再向集线器的所有端口提交,这会造成信号之间碰撞的机会很大,而且信号也很有可能被窃听。这还意味着所有连接到该集线器的设备,都是属于同一个碰撞域和广播域,因此大部分集线器现在都已经被交换机所取代。</p><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>集线器工作在物理层,负责局域网内部通信,将其接收到的数据帧广播至全部端口,与其相连的全部网络节点属于同一个冲突域。</p><h2 id="网桥"><a href="#网桥" class="headerlink" title="网桥"></a>网桥</h2><hr><h3 id="基本原理-1"><a href="#基本原理-1" class="headerlink" title="基本原理"></a>基本原理</h3><p>网桥(Bridge)负责数据链路层的数据中继,互连两个独立的、仅在低两层(物理层和数据链路层)实现上有差异的子网。</p><h3 id="核心功能-1"><a href="#核心功能-1" class="headerlink" title="核心功能"></a>核心功能</h3><p>数据链路层的作用包括:链路建立、维护和拆除、帧封装、帧传输、帧同步、帧错误控制以及流量控制。网桥工作在数据链路层,将两个局域网(LAN)连接起来,根据MAC地址(物理地址)来转发帧,可以看做是一个“低层路由器”(说路由器是三层交换机是一个道理,路由器工作在网络层,根据网络地址即IP地址进行转发)。它可以有效连接两个LAN,将本地通信范围限制在本网段内,并转发相应的信号到另一网段。网桥通常用于连接数量不多的、同一类型的网段。</p><h3 id="隔离冲突域"><a href="#隔离冲突域" class="headerlink" title="隔离冲突域"></a>隔离冲突域</h3><p>多个集线器连接在一起时,由于是广播通信,碰撞几率极大,所以需要一种设备,能够有效隔离子网,即不同网段,让各网段的广播通信仅仅发生在自身内部,网桥正好起到了隔离冲突域的作用。</p><h3 id="存储转发"><a href="#存储转发" class="headerlink" title="存储转发"></a>存储转发</h3><p>网桥能够识别数据链路层中的数据帧,并将这些帧临时存储于自身内存之中,再重新生成信号作为一个全新的数据帧转发给相连的另一个网段,由于网桥可以对数据帧进行拆包、暂存和重新打包(称为“存储转发机制”,Store-and-Forward),网桥能够连接不同技术参数、传输速率的数据链路。</p><h3 id="优缺点"><a href="#优缺点" class="headerlink" title="优缺点"></a>优缺点</h3><p>网桥的存储和转发功能与中继器相比有优点也有缺点:</p><ul><li><p>优点:使用网桥互连克服了物理限制,这意味着构成LAN的数据站总数和网段数很容易扩充。网桥纳入存储和转发功能可使其适应于连接使用不同MAC协议的两个LAN,从而构成一个不同LAN混连在一起的网络环境。网桥的中继功能仅仅依赖于MAC地址(具有交换机一样的转发机制,通过自学习维护转发表(MAC地址到网桥端口的映射),根据目的MAC地址查找转发表决定要转发至的端口),因此对高层协议完全透明。网桥将一个较大的LAN分成段,有利于改善可靠性、可用性和安全性。</p></li><li><p>缺点:由于网桥会在执行转发之前先接收帧并进行缓冲,与中继器相比会引入更多时延。并且网桥不提供流量控制功能,在流量爆发时容易过载,严重时会丢失帧。123</p></li></ul><p>不过,网桥的优点多过于缺点正是其广泛使用的原因。</p><h3 id="小结-1"><a href="#小结-1" class="headerlink" title="小结"></a>小结</h3><p>网桥工作在数据链路层,负责跨局域网的主机通信,根据其所维护转发表(MAC表)完成转发,隔离冲突域。局域网内部通信不过网桥。</p><p>网桥相当于二层交换机,它可以在数据链路层“桥接”两个网段。它比HUB强一点的在于它分离了两个网段,不会把某网段内部(LAN内部)的数据包广播到另一个网段。因此,两个网段之间不会产生不必要的信号冲突碰撞,例如:AB在桥东,CD在桥西,AB对话时,CD也可以对话;但AC对话时,BD要避让。</p><h2 id="交换机"><a href="#交换机" class="headerlink" title="交换机"></a>交换机</h2><hr><h3 id="基本原理-2"><a href="#基本原理-2" class="headerlink" title="基本原理"></a>基本原理</h3><p>交换机(Switch)是一种简化的网桥,互连相同类型的网络,也工作在数据链路层。它采用独享带宽的工作方式,比集线器更智能,它关注数据包的MAC地址部分:目的地址和源地址,采用一种“自学习”的方式:根据收到的数据帧中的源MAC地址以及其进入交换机的端口两者之间建立映射,并将其写入所维护的一张MAC转发表(Forwarding Table based on MAC),定期更新或废弃表项。每次新到来一个数据包,交换机抽取其头部中的目的MAC地址,查找转发表对应项,随后将该数据包转发到表项指示的交换机端口。而对于无法在转发表中找到对应项的数据帧,尤其是在交换机刚刚加入网络初期时,交换机便将此帧转发至除其进入端口以外的全部交换机端口。</p><h3 id="工作流"><a href="#工作流" class="headerlink" title="工作流"></a>工作流</h3><p>特别地,对于转发表项指示将该数据帧发往此帧进入交换机的端口时,表明是源主机想向同一个LAN中的目的主机发包,通常LAN由集线器连接多个主机构成,而一旦其中某个主机想要通信,首先会把数据包发往与其直连的集线器,而集线器会进行全端口转发,这样与该集线器相连的,也就是同一局域网段内的所有主机实际都收到了一份数据包的一份拷贝,所以如果该数据包最终被集线器也广播至交换机处,那么交换机通过查找转发表发现源端口和转发端口是一样的,那么交换机就知道该包之前已在同一网段中广播,也就是说目的主机早就收到了数据包,因此,面对这样的数据包,交换机的处理方法很简单:直接丢弃。</p><h3 id="物理构造"><a href="#物理构造" class="headerlink" title="物理构造"></a>物理构造</h3><p>在交换机内部存有一条背部总线和内部交换矩阵,其中,背部总线用于连接交换机的所有端口,内部交换矩阵用于查找数据包所需传送的目的地址所在端口。控制电路受到数据包后,首先通过内部交换矩阵对其目的端口进行查询,若查询到则立刻将数据包发往该端口,若没有查询到,则广播至所有端口,接受端口发出回应后,将数据包发往该端口,并将其添加至内部交换矩阵中。</p><h3 id="自学习"><a href="#自学习" class="headerlink" title="自学习"></a>自学习</h3><p>交换机的转发表(又称为MAC表)是自动地、动态地、自治地建立的,即没有任何来自网络管理员或配置协议的任何干预,因此,通常称交换机创建并维护转发表的过程称为是“自学习”(Self-learning)的,具体步骤如下:</p><ol><li>交换机初始化为空,即没有任何表项; </li><li>对于在某接口接收到的每个入帧,该交换机为其在转发表中存储①帧源地址字段中的源MAC地址②该帧到达的交换机端口③当前时间戳,交换机以这种方式在它的表中记录发送节点所在的LAN字段(与某个交换机端口直接相连),若在LAN中的每个节点最终都发送了一个帧,则每个节点在转发表中都会被记录下来;</li><li>如果在一段时间,称为“老化期”(Aging Time)之后(此时间由交换机中的一个定时器维护),交换机都没有接收到以某个表项中的MAC地址作为源地址的帧,那么就删除该表项。</li></ol><p>由于交换机只会将数据包发往转发表中该目的MAC对应的端口,而不是广播至其所有端口,因此,交换机可以用来隔离链路层广播域,即冲突域,每个交换机端口都与一个冲突域相连。但它工作在数据链路层,从而无法处理网络地址,如IP地址,因此无法划分网络层广播域,即广播域。</p><ul><li>冲突域:在同一个冲突域中的每一个节点都能收到任意内部节点发送的任何帧(会通过集线器转发),即冲突域是一个网段(LAN),或者说该网段内全部节点的集合;</li><li>广播域:网络中能接收到任意设备发出的广播帧的所有设备的集合。</li></ul><p>看到这里,势必需要把网桥和交换机比较一番:</p><ol><li>从网络数据包的转发层面,两者都是通过建立、维护和查询转发表完成转发工作;</li><li>网桥的端口数较少,通常只有2个,当然也有多端口的网桥设备,交换机则一般拥有多个物理端口;</li><li>交换机工作时,实际上允许许多组端口间的数据转发通道同时工作,相当于多个网桥集成在一个设备中同时完成转发工作,所以交换机的转发能力远强于一般仅有两个端口的网桥;</li><li>由于交换机能够支持多端口,因此可以把网络系统划分为更多的物理网网段,使得整个网络系统具有更高的带宽;</li><li>交换机内部一般使用<a href="https://en.wikipedia.org/wiki/Application-specific_integrated_circuit" target="_blank" rel="noopener">ASIC(Application Specific Integrated Circuit, 特定用途集成电路)</a>的硬件芯片来实现转发,同时由于是硬件转发,其转发性能非常高,数据传输速率要快于网桥;</li><li>网桥在发送数据帧前,通常要接收到完整的数据帧并执行帧检测序列FCS后,才开始转发该数据帧。交换机具有存储转发和直接转发两种帧转发方式。直接转发方式在发送数据以前,不需要在接收完整个数据帧和经过32BIT循环冗余校验码CRC的计算检查后的等待时间。</li></ol><h3 id="小结-2"><a href="#小结-2" class="headerlink" title="小结"></a>小结</h3><p>交换机可以认为是网桥的全方位加强版:同样通过自学习维护转发表确定数据帧转发的目的端口,用来隔离冲突域,每个与交换机的一个物理端口相连的LAN就是一个冲突域。但交换机拥有更多物理端口从而支持更多网段,其多组端口并行工作以及通过ASIC硬件芯片转发从而具有更强的转发能力(数据帧处理能力、传输速率)。</p><h2 id="路由器"><a href="#路由器" class="headerlink" title="路由器"></a>路由器</h2><hr><h3 id="基本原理-3"><a href="#基本原理-3" class="headerlink" title="基本原理"></a>基本原理</h3><p>路由器(Router)负责网络层的数据中继。它能够理解数据包头部字段中的IP地址(又称为网络地址,或者相对于MAC地址被称为物理地址,也可称作是逻辑地址),如果它接收到一个数据包,就会首先检查其中的IP地址,查询其维护的路由表(Routing Table)中对应表项,如果目标地址是本地网络的就不予理会,而如果是在其它网络中的某台主机,就把该数据包转发出本地网络。</p><h3 id="核心功能-2"><a href="#核心功能-2" class="headerlink" title="核心功能"></a>核心功能</h3><p>路由器的作用在于连接不同类型(相同当然可以)的网络,并且能够在网络中通过运行路由算法为数据包传输找出最合适的路径(代价最小),这一过程称作“路由选择”。</p><p>路由器的存储器里存放着路由表,这些表是易失的并且容易改变,路由表项的内容包括目的地址的下一跳(Next Hop)的路由地址,不同地址的距离等。这些内容都是路由器启动后经过学习得到的。路由器启动之后便根据设定的路由协议与其它路由器交换信息,在交换信息的过程中,学习路由并填充路由表。</p><p>因此,路由器虽然是基于硬件转发数据包的专用网络设备,但是路由器上配置支持的路由算法才是最核心的东西。</p><h3 id="选路算法"><a href="#选路算法" class="headerlink" title="选路算法"></a>选路算法</h3><p>选路/路由算法分为两大类:</p><ol><li><p>距离向量算法(Distance Vector Algorithm, 即DV算法)</p><ul><li>RIP(Routing Information Protocol,路由信息协议)</li><li>IGRP(Interior Gateway Routing Protocol,内部网关路由协议)</li></ul></li><li><p>链路状态算法(Link State Algorithm,即LS算法)</p><ul><li>OSPF(Open Shortest Path First Interior Gateway Protocol,开放式最短路径优先内部网关协议 )</li></ul></li></ol><p>还有一种综合两种算法的混合路由方案,如EIGRP(Enhanced Interior Gateway Routing Protocol,加强型内部网关路由协议)。</p><p>路由器使用距离矢量算法,判断到达目的地址的优先路径的标准就只有一个,那就是跳数,认为具有最小跳数的路径是最短/优路径,而不理会其带宽,可靠性,时延等因素。并且认为跳数大于15跳的目的地址是不可到达的。</p><p>另外,路由还有一个二层设备不具有的功能,那就是隔绝广播,它可以将广播限制在一个网络之内,进而增大网络之间的带宽。</p><h3 id="小结-3"><a href="#小结-3" class="headerlink" title="小结"></a>小结</h3><p>路由器工作在网络层,负责连接不同类型的网络,构造广域网(Wide Area Network, WAN),隔离广播域,根据数据包的IP地址查找路由表对应表项完成转发。</p><h2 id="网关"><a href="#网关" class="headerlink" title="网关"></a>网关</h2><hr><h3 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h3><p>网关(Gateway)负责网络层以上的数据中继服务,实现不同体系结构的网络协议转换,它通常采用<strong>软件</strong>的方法实现,并且与特定的应用服务一一对应。比如:OSI的文件传输服务FTAM与TCP/IP的文件传输服务FTP,尽管二者都是文件传输但是由于所执行的协议不同不能直接进行通信,而需要网关将两个文件传输系统互连,达到相互进行文件传输的目的。 </p><p>网关是一种复杂的网络连接设备,可以支持不同协议之间的转换,实现不同协议网络之间的互连。<strong>网关具有对不兼容的高层协议进行转换的能力,为了实现异构设备之间的通信,网关需要对不同的链路层、专用会话层、表示层和应用层协议进行翻译和转换</strong>。所以网关兼有路由器、网桥、中继器的特性。</p><p>若要使两个完全不同的网络(异构网)连接在一起,一般使用网关,在Internet中两个网络也要通过一台称为网关的计算机实现互联。这台计算机能根据用户通信目标计算机的IP地址,决定是否将用户发出的信息送出本地网络,同时,它还将外界发送给属于本地网络计算机的信息接收过来,它是一个网络与另一个网络相联的通道。为了使TCP/IP协议能够寻址,该通道被赋予一个IP地址,这个IP地址称为网关地址。</p><h3 id="小结-4"><a href="#小结-4" class="headerlink" title="小结"></a>小结</h3><p>网关的作用就是将两个使用不同协议的网络段连接在一起的设备,对两个网络段中的使用不同传输协议的数据进行互相的翻译转换。在互连设备中,由于协议转换的复杂性,一般只能进行一对一的转换,或是少数几种特定应用协议的转换。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><hr><p>[1] <a href="http://blog.csdn.net/gongda2014306/article/details/52442981" target="_blank" rel="noopener">网络设备解析:中继器、集线器、网桥、交换机、路由器、网关的区别</a><br>[2] <a href="http://blog.csdn.net/u012243115/article/details/47008903" target="_blank" rel="noopener">转发器、集线器、网桥、交换机、路由器和网关简介</a><br>[3] <a href="http://blog.csdn.net/zhongyou2009/article/details/4768807" target="_blank" rel="noopener">交换机和网桥的区别</a><br>[4] <a href="http://hextwolf.blog.51cto.com/59501/29696" target="_blank" rel="noopener">广播域与冲突域详细解析</a><br>[5] <a href="http://www.nowamagic.net/academy/detail/72150314" target="_blank" rel="noopener">集线器、交换机和路由器通俗点的解释</a><br>[6] <a href="http://www.eepw.com.cn/article/275540.htm" target="_blank" rel="noopener">交换机工作原理</a><br>[7] <a href="http://blog.sina.com.cn/s/blog_5e1a889d0101370d.html" target="_blank" rel="noopener">交换机的工作原理(二、三、四层交换原理)</a></p>]]></content>
<categories>
<category> Network </category>
</categories>
<tags>
<tag> Interconnect </tag>
<tag> Switch </tag>
<tag> Router </tag>
</tags>
</entry>
<entry>
<title>P4学习笔记:背景起源与基本原理</title>
<link href="/2017/10/28/Network/SDN/Language/P4/origin-background/"/>
<content type="html"><![CDATA[<h2 id="全文概要"><a href="#全文概要" class="headerlink" title="全文概要"></a>全文概要</h2><hr><p>软件定义网络(<a href="https://en.wikipedia.org/wiki/Software-defined_networking" target="_blank" rel="noopener"><strong>Software Defined Network, SDN</strong></a>)核心在于网络转发设备的数据转发平面(<strong>Data Plane</strong>)和控制平面(<strong>Control Plane</strong>)的分离和解耦,以实现可编程按需定制、集中式统一管理、动态流量监控、自动化部署。2014年<a href="https://p4.org/" target="_blank" rel="noopener"><strong>P4</strong></a>语言横空出世,进一步提高了面向交换机编程的可行性和效率,成为SDN领域的又一里程碑式的成果。本文从宣告P4诞生的<a href="https://www.sigcomm.org/" target="_blank" rel="noopener"><strong>SIGCOMM</strong></a>会议论文出发,介绍其背景起源、基本原理以及未来广阔的应用前景。<br><a id="more"></a></p><h2 id="行业趋势"><a href="#行业趋势" class="headerlink" title="行业趋势"></a>行业趋势</h2><hr><p><strong>SDN is the future, and P4 defines it.<br>SDN是未来,P4定义未来。</strong></p><h2 id="相关论文"><a href="#相关论文" class="headerlink" title="相关论文"></a>相关论文</h2><hr><p>Proposed in <a href="https://www.sigcomm.org/publications/computer-communication-review" target="_blank" rel="noopener">SIGCOMM Communication Review[J]</a>, 2014.<br>-<a href="https://arxiv.org/pdf/1312.1719.pdf" target="_blank" rel="noopener"><strong>《P4: Programming Protocol-Independent Packet Processors》</strong></a></p><h2 id="背景起源"><a href="#背景起源" class="headerlink" title="背景起源"></a>背景起源</h2><hr><p>P4是由P.Bosshart等人提出的一种用于处理数据包转发的高层抽象语言,协议独立(<strong>Protocol-Independent</strong>)是其核心特点,如OpenFlow一样是一种<a href="https://zhuanlan.zhihu.com/p/26743952" target="_blank" rel="noopener"><strong>南向协议</strong></a>,但是其应用范围要比OpenFlow还要大:不仅可以指导数据流转发,更提供了对交换机等网络转发设备的SDN数据平面的编程接口,实现了在设备层对数据处理流程进行软件定义,是真正意义上的软件定义网络。</p><p>就如论文中所说:</p><blockquote><p>We propose P4 as a strawman proposal for how OpenFlow should evolve in the Future.</p></blockquote><p>目前,P4语言作为一种潜在的<strong>OpenFlow2.0</strong>的发展方向在努力。</p><p>与华为<a href="http://www.poforwarding.org" target="_blank" rel="noopener"><strong>POF(Protocol Oblivious Forwarding)</strong></a>提出目的类似,P4的提出的目的也是为了解决OpenFlow可编程能力不足及其协议设计本身所带来的高复杂度(<strong>Complexity</strong>)和较差可扩展性(<strong>Scalability</strong>)的难题。</p><p>自从OpenFlow 1.0发布以来,其版本目前已演进到<a href="https://www.opennetworking.org/wp-content/uploads/2014/10/openflow-switch-v1.5.1.pdf" target="_blank" rel="noopener"><strong>1.5</strong></a>,为了兼容多种不同的网络协议,使OpenFlow交换机能够处理具有不同头部的数据包,其匹配域(<strong>Header Field</strong>)的个数从1.0版本的12元组,变成1.3版本的40个,到最新1.5版本的45个,其匹配域数目随着新版本支持特性的更新不断增加,OpenFlow匹配域个数随版本演变情况具体见下表:</p><style> table th:nth-of-type(1) { width: 150px; }</style><style> table th:nth-of-type(2) { width: 150px;}</style><table><thead><tr><th style="text-align:center">Version</th><th style="text-align:center">Date</th><th style="text-align:center">Header Fields</th></tr></thead><tbody><tr><td style="text-align:center">OF 1.0</td><td style="text-align:center">Dec 2009</td><td style="text-align:center">12 fields(Ethernet, TCP/IPv4)</td></tr><tr><td style="text-align:center">OF 1.1</td><td style="text-align:center">Feb 2011</td><td style="text-align:center">15 fields(MPLS, inter-table metadata)</td></tr><tr><td style="text-align:center">OF 1.2</td><td style="text-align:center">Dec 2011</td><td style="text-align:center">36 fields(ARP, ICMP, IPv6, etc)</td></tr><tr><td style="text-align:center">OF 1.3</td><td style="text-align:center">Jun 2012</td><td style="text-align:center">40 fields</td></tr><tr><td style="text-align:center">OF 1.4</td><td style="text-align:center">Oct 2013</td><td style="text-align:center">41 fields</td></tr><tr><td style="text-align:center">OF 1.5</td><td style="text-align:center">Dec 2014</td><td style="text-align:center">45 fields</td></tr></tbody></table><p>但OpenFlow本身并不支持弹性增加匹配域,因此每次增加一个匹配域就需要重新编写控制器(<strong>Controller</strong>)和交换机两端的协议栈,以及交换机的数据包处理逻辑,并分别烧制到OpenFlow控制器和交换机芯片上,这无疑大大增加了交换机设计的难度,高昂的更新成本也严重影响OpenFlow协议的版本稳定性,阻碍了OpenFlow的推广。</p><h2 id="语言特性"><a href="#语言特性" class="headerlink" title="语言特性"></a>语言特性</h2><hr><p>根据论文所述,P4致力于实现以下三个目标:</p><ol><li><p><strong>可重配置性(Reconfigurability)</strong></p><blockquote><p>Programmers should be able to change the way switches process packets once they are deployed.</p></blockquote><p> 可灵活定义转发设备数据处理流程,且能够做到转发无中断的重配置。OpenFlow能够在已经固化在交换机上的数据处理逻辑之上,通过流表项指导数据流转发处理,而无法重新定义交换机处理数据的逻辑,每次增加新的网络协议支持,都必须将相关设备宕机下线并重新配置后再重新部署上线,即无法在不中断其它数据包转发的情况下弹性支持新协议。而P4语言的<strong>卖点</strong>恰恰在于其拥有对交换机的数据平面,即数据包处理逻辑即时编程的能力。</p></li><li><p><strong>协议无关性(Protocol-Independence)</strong></p><blockquote><p>Switches should not be tied to any specific network protocols.</p></blockquote><p> 交换机等转发设备无需关心协议语法和语义等内容,依然能够完成数据转发任务。这是使用P4可以自定义数据处理逻辑,并通过控制器对交换机等转发设备编程配置实现对应的协议处理逻辑,而这个行为(<strong>Behavior</strong>)将被翻译成对应的匹配(<strong>Match</strong>)和动作(<strong>Action</strong>),从而被转发设备理解和执行。</p></li><li><p><strong>目标设备无关性(Target-Independence)</strong></p><blockquote><p>Programmers should be able to describe packet-processing functionality independently of the underlying hardware.</p></blockquote><p> 正如写C和Java(CPU-oriented Languauge,面向CPU编程语言)代码时并不需要了解CPU的相关信息(型号、参数等等硬件细节),使用P4语言进行网络编程同样无需关心底层转发设备的具体信息。P4的编译器(Compiler)会将通用的P4语言处理逻辑翻译成设备所能理解的机器指令,并将其写入转发设备,完成配置和编程。</p></li></ol><h2 id="基本原理"><a href="#基本原理" class="headerlink" title="基本原理"></a>基本原理</h2><hr><h3 id="转发模型-Forwarding-Model"><a href="#转发模型-Forwarding-Model" class="headerlink" title="转发模型(Forwarding Model)"></a>转发模型(Forwarding Model)</h3><p>论文中提出的数据包转发抽象模型如下图所示,交换机通过一个可编程的解析器(<strong>Parser</strong>)以及其后紧跟的多个”匹配-动作”操作阶段,或顺序,或并行,或组合两种模式,完成数据包的转发任务。<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/SDN/Language/P4/origin-background/forwarding-model.png" alt="image"></p><p>模型概括了数据包是如何在<strong>不同转发设备</strong>(包括:以太网交换机、负载均衡器、路由器)中,通过<strong>不同技术</strong>(包括:固定功能交换机ASIC芯片、网络处理器(Network Processor Unit,NPU)、现场可编程门阵列(Field Programmable Gate Array,FPGA)、可重配置交换机、软件交换机)处理的。</p><p>基于此转发模型,设计出P4这样一种数据平面编程语言,用于描述数据包处理逻辑。从此,开发人员只需编写高层抽象的目标无关(<strong>Target-Inpendent</strong>)的P4程序,而无需关心底层设备细节,编译器会负责将程序映射到不同的底层转发设备,支持设备的种类范围从转发相对较慢的软件交换机到最快的基于ASIC的交换机,编译器都能将P4程序最终翻译成设备能理解并执行的机器指令。</p><p>转发模型由两种类型的操作所控制:</p><ol><li><strong>配置(Configure)</strong>:对解析器编程,设置各个匹配-动作阶段的执行顺序,并且指定每个阶段应处理的头部字段;</li><li><strong>部署(Populate)</strong>:动态增加、删除在配置阶段创建的匹配-动作表中的条目(<strong>Entry</strong>)。</li></ol><p><strong>配置操作(Configuration)</strong>决定了网络转发设备所支持的协议和数据包处理的逻辑,而<strong>部署操作(Population)</strong>决定了在给定时间内对数据包应用的处理策略(<strong>Policy</strong>)。配置与部署实际被设计成两个不同的阶段(<strong>Phase</strong>),在配置阶段交换机不需要能够处理数据包,但是需要在部分或者全部重新配置(<strong>Reconfigure</strong>)阶段依然能够做到无宕机,即无中断的数据包处理。</p><p>根据转发模型,数据包到达交换机后,先由解析器处理,由于匹配阶段只需要头部信息(<strong>Packet Header</strong>),故将数据体(<strong>Packet Body</strong>)先分别缓存。解析器识别并提取出特定头部字段,这实际定义了交换机支持的协议类型,在这些字段上面,会执行匹配操作,并根据匹配结果执行相关动作。</p><p>被提取的字段随后被传送给匹配-动作表,表被分成两个部分:入口表(<strong>Ingress Table</strong>)和出口表(<strong>Egress Table</strong>)。两者都可能会修改数据包头部,入口表决定了数据包的输出端口及其应该被放置的队列。被入口表处理时,数据包可能会被转发、复制(多播、发送至控制平面)、丢弃或触发流量控制。出口表执行对数据包头部的修改,如针对多播数据包的修改。</p><p>在不同处理阶段之间,数据包还可以携带额外信息,称作”元数据“,常见的元数据例如:输入端口、目的地址、队列、时间戳和虚拟网络标识符等等。</p><p>排队机制(<strong>Queuing Discipline</strong>)则是借鉴了OpenFlow中的做法:一个动作将一个数据包映射到一个队列,每个队列都会接受某种在交换机配置阶段就选定的服务类型(<strong>Service Discipline,受限水平这里不太理解,还请指教~</strong>)的数据包。</p><h4 id="P4-VS-OpenFlow"><a href="#P4-VS-OpenFlow" class="headerlink" title="P4 VS OpenFlow"></a>P4 VS OpenFlow</h4><p>与OpenFlow相比,P4的设计有三个优点:</p><ol><li>P4可编程定制数据解析流程,即<strong>Programmable Parser</strong>,而OpenFlow交换机只支持固定的包处理解析逻辑,即<strong>Fixed Parser</strong>;</li><li>P4可执行串行(<strong>Serial</strong>)和并行(<strong>Parallel</strong>)的Match-Action操作,而OpenFlow仅支持串行操作;</li><li>由于P4模型包含程序编译器,负责完成将P4程序到具体交换设备配置的映射,从而支持协议无关的转发,而OpenFlow支持的协议需要在初始时配置,此后每次修改都需要宕机,编写新的协议数据包处理逻辑再配置到交换机,不能做到无转发中断的弹性增加所支持的协议。<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/SDN/Language/P4/origin-background/p4-vs-openflow.png" alt="image"></li></ol><h3 id="核心部件-Key-Component"><a href="#核心部件-Key-Component" class="headerlink" title="核心部件(Key Component)"></a>核心部件(Key Component)</h3><h4 id="头部-Header"><a href="#头部-Header" class="headerlink" title="头部(Header)"></a>头部(Header)</h4><p>对数据包的处理都需要根据包头的字段内容来决定对其采取什么操作,所以在P4程序中需要定义对应的包头。</p><blockquote><p>A header definition describes the sequence and structure of a series of fields. It includes specification of field widths and constraints on field values.</p></blockquote><p>包头本质上就是有序排列的字段序列,包头由有序的字段名称即对应的字段长度组成,其中以太网和VLAN的包头格式如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">header ethernet {</span><br><span class="line"> fields {</span><br><span class="line"> dst_addr : <span class="number">48</span>; <span class="comment">// width in bits</span></span><br><span class="line"> src_addr : <span class="number">48</span>;</span><br><span class="line"> ethertype : <span class="number">16</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">header vlan {</span><br><span class="line"> fields {</span><br><span class="line"> pcp : <span class="number">3</span>;</span><br><span class="line"> cfi : <span class="number">1</span>;</span><br><span class="line"> vid : <span class="number">12</span>;</span><br><span class="line"> ethertype : <span class="number">16</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>下面是一个称作<strong>mTag</strong>的头部定义,它能够在数据包被处理的过程中在不影响已有字段声明的情况下被添加至其头部,携带额外的处理信息,作为我们的一个P4程序的完整实例的一部分引入。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">header mTag {</span><br><span class="line"> fields {</span><br><span class="line"> up1: <span class="number">8</span>;</span><br><span class="line"> up2: <span class="number">8</span>;</span><br><span class="line"> down1: <span class="number">8</span>;</span><br><span class="line"> down2: <span class="number">8</span>;</span><br><span class="line"> ethertype: <span class="number">16</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="解析器-Parser"><a href="#解析器-Parser" class="headerlink" title="解析器(Parser)"></a>解析器(Parser)</h4><p>在定义了包头之后,还需要定义头部字段之间的关系,及数据包解析的对应关系。</p><blockquote><p>A parser definition specifies how to identify headers and valid header sequences within packets.</p></blockquote><p>比如以太网头部的<strong>ethertype</strong>字段在等于<strong>0x0800</strong>时应该继续跳转至IPv4的头部进行后续解析。下面仍以以太网头部解析(<strong>parser ethernet</strong>表示本解析器专门用来解析以太网头部)为例:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">parser ethernet {</span><br><span class="line"> <span class="keyword">switch</span>(ethertype)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">case</span> <span class="number">0x8100</span>: vlan;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">0x9100</span>: vlan;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">0x0800</span>: ipv4;</span><br><span class="line"> <span class="comment">// Other cases</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>P4假设底层交换机能实现一个从头到尾遍历数据包头部的状态机(<strong>State Machine</strong>),并在该遍历过程中依次提取(<strong>Extract</strong>)出头部各字段内容,这些字段内容最终会被送到下面介绍的Match-Action表中统一处理。</p><p>P4将此状态机描述为一个从一个头部字段到下一个头部字段的转换(<strong>Transition</strong>)的集合,每次转换由当前头部字段的具体取值所触发。下面我们会描述一个<strong>mTag</strong>状态机:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line">parser start {</span><br><span class="line"> ethernet;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">parser ethernet {</span><br><span class="line"> <span class="keyword">switch</span>(ethertype)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">case</span> <span class="number">0x8100</span>: vlan;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">0x9100</span>: vlan;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">0x0800</span>: ipv4;</span><br><span class="line"> <span class="comment">// Other cases</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">parser vlan {</span><br><span class="line"> <span class="keyword">switch</span>(ethertype)</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">case</span> <span class="number">0xaaaa</span>: mTag;</span><br><span class="line"> <span class="keyword">case</span> <span class="number">0x0800</span>: ipv4;</span><br><span class="line"> <span class="comment">// Other cases</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">parser mTag {</span><br><span class="line"> <span class="keyword">switch</span>(ethertype) {</span><br><span class="line"> <span class="keyword">case</span> <span class="number">0x0800</span>: ipv4;</span><br><span class="line"> <span class="comment">// Other cases</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>注意,所有的解析均从start状态开始,并在到达stop状态或出错之后结束。解析器用于将字节流数据解析为对应的协议报文,用于后续的流表项匹配和动作执行。</p><p>一旦解析来到一个新的头部,即到达一个新状态,状态机利用其配置信息(<strong>Specification</strong>)将头部提取出来,并确定其下一次转换。被提取出的头部被转发至交换机流水线后半段执行的Match-Action操作处理。</p><h4 id="匹配-动作表-Match-Action-Table"><a href="#匹配-动作表-Match-Action-Table" class="headerlink" title="匹配-动作表(Match-Action Table)"></a>匹配-动作表(Match-Action Table)</h4><p>P4中需要定义多种类型用途的表用于存储匹配表项,格式为Match+Action,即匹配域(头部部分字段的组合)和对应的执行动作。P4语言定义某个表具体的匹配域及需要执行的动作。而具体的流表项会在网络运行过程中通过控制器来编程下发,从而完成对应数据流的处理。因此,匹配-动作表,实际定义或决定了相关数据包的处理逻辑。</p><blockquote><p>Match+action tables are the mechanism for performing packet processing. The P4 program defines the fields on which a table may match and the actions it may execute.</p></blockquote><p>接着上文中<strong>mTag</strong>的例子,边缘交换机(<strong>Edge Switch</strong>)会匹配二层目的地址(数据链路层目的地址,即目的MAC地址)和VLAN ID,并且将<strong>mTag</strong>添加到数据包头部, 从而数据包在交换网络中就可以通过匹配<strong>mtag</strong>来完成转发。下面P4程序定义了一个表来<strong>匹配</strong>上述字段,并且对数据包应用一个添加<strong>mTag</strong>头部的<strong>动作</strong>。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">table mTag_table {</span><br><span class="line"> reads {</span><br><span class="line"> ethernet.dst_addr: exact;</span><br><span class="line"> vlan.vid: exact;</span><br><span class="line"> }</span><br><span class="line"> actions {</span><br><span class="line"> <span class="comment">// At runtime, entries are programmed with params</span></span><br><span class="line"> <span class="comment">// for the mTag action, see below.</span></span><br><span class="line"> add_mTag;</span><br><span class="line"> }</span><br><span class="line"> max_size: <span class="number">20000</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>表中各属性意义如下:</p><ul><li><strong>reads</strong>:声明匹配域,即需要匹配的字段,由匹配类型量化,包括:<strong>exact</strong>(精确匹配,完全匹配)、<strong>ternary</strong>、<strong>ranges</strong>(一定范围内)、wildcard(通配符);</li><li><strong>actions</strong>: 列举本表可能对被匹配的数据包采取的动作,动作会在下一小节重点阐述;</li><li><strong>maxsize</strong>: 规定本表所能支持的最大条目容量。</li></ul><p>为了在后文继续讨论<strong>mTag</strong>的后续处理流程,还定义了如下三个表:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line">table source_check {</span><br><span class="line"> <span class="comment">// Verify mtag only on ports to the core</span></span><br><span class="line"> reads {</span><br><span class="line"> mtag: valid; <span class="comment">// Was mtag parsed?</span></span><br><span class="line"> metadata.ingress_port: exact;</span><br><span class="line"> }</span><br><span class="line"> actions {</span><br><span class="line"> <span class="comment">// Each table entry specifies one action</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// If inappropriate mTag, send to CPU</span></span><br><span class="line"> fault_to_cpu;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// If mtag found, strip and record in metadata</span></span><br><span class="line"> strip_mtag;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Otherwise, allow the packet to continue</span></span><br><span class="line"> pass;</span><br><span class="line"> }</span><br><span class="line"> max_size: <span class="number">64</span>; <span class="comment">// One rule per port</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">table local_switching {</span><br><span class="line"> <span class="comment">// Reads destination and checks if local</span></span><br><span class="line"> <span class="comment">// If miss occurs, goto mtag table</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">table egress_check {</span><br><span class="line"> <span class="comment">// Verify egress is resolved</span></span><br><span class="line"> <span class="comment">// Do not retag packets received with tag</span></span><br><span class="line"> <span class="comment">// Reads egress and whether packet was mTagged</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="动作-Action"><a href="#动作-Action" class="headerlink" title="动作(Action)"></a>动作(Action)</h4><p>与OpenFlow的动作类似,不过P4程序中的动作是抽象程度更高的协议无关的操作。P4定义了一套协议无关的原始指令集(<strong>Primitive</strong>,原语),基于该指令集可以实现复杂的协议操作,这可以通过赋予不同的参数来调用这些原始指令集组合来实现,而这些参数还可以是数据包匹配过程中产生的元数据。</p><p>P4假定一个动作函数(<strong>Action Function</strong>)里的原语都是可并行执行的,而原本不支持并行的交换机则可以通过仿真来实现类似效果(缺乏对交换机硬件结构的详细理解,暂留疑问,欢迎指教~)。</p><blockquote><p>P4 supports construction of complex actions from simpler protocol-independent primitives. These complex actions are available within match+action tables.</p></blockquote><p>上文提到的<strong>add_mTag</strong>动作用P4表述如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">action <span class="title">add_mTag</span><span class="params">(up1, up2, down1, down2, egr_spec)</span> </span>{</span><br><span class="line"> add_header(mTag);</span><br><span class="line"> <span class="comment">// Copy VLAN ethertype to mTag</span></span><br><span class="line"> copy_field(mTag.ethertype, vlan.ethertype);</span><br><span class="line"> <span class="comment">// Set VLAN's ethertype to signal mTag</span></span><br><span class="line"> set_field(vlan.ethertype, <span class="number">0xaaaa</span>);</span><br><span class="line"> set_field(mTag.up1, up1);</span><br><span class="line"> set_field(mTag.up2, up2);</span><br><span class="line"> set_field(mTag.down1, down1);</span><br><span class="line"> set_field(mTag.down2, down2);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Set the destination egress port as well</span></span><br><span class="line"> set_field(metadata.egress_spec, egr_spec);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>从以上程序可以看出<strong>mTag</strong>动作的执行流程:交换机将<strong>mTag</strong>插入到<strong>VLAN tag</strong>之后,将<strong>VLAN tag</strong>的<strong>ethertype</strong>赋值给<strong>mTag</strong>的对应字段从而暗示(Indicate)后面接的字段是<strong>VLAN</strong>头部,然后将<strong>VLAN tag</strong>的<strong>ethertype</strong>赋值为<strong>0xaaaa</strong>来唤醒<strong>parser mTag</strong>处理<strong>mTag</strong>这个新增头部。</p><p>P4支持的原语动作集包括:</p><ul><li><strong>set_field</strong>:为头部中某个特定字段赋值;</li><li><strong>copy_field</strong>:将参数二的字段值赋给参数一代表的字段;</li><li><strong>add_header</strong>:将一个特定的头部实例(或其包含的全部字段)设为有效(valid)的;</li><li><strong>remove_header</strong>:从一个数据包删除其头部(或其头部包含的全部字段);</li><li><strong>increment</strong>:增加或减小一个字段的值;</li><li><strong>checksum</strong>:为一个字段集合计算校验和(如IPv4头部,根据其全部字段计算校验和)。</li></ul><h4 id="控制程序-Control-Program"><a href="#控制程序-Control-Program" class="headerlink" title="控制程序(Control Program)"></a>控制程序(Control Program)</h4><p>一旦表和动作都已完成定义,剩下的任务就是指定从一个表到下一个表的控制流。在P4程序中,控制流通过一系列函数(<strong>Functions</strong>)、条件(<strong>Conditionals</strong>)和表引用(<strong>Table References</strong>)来指定。</p><blockquote><p>The control program determines the order of match+action tables that are applied to a packet. A simple imperative program describe the flow of control between match+action tables.</p></blockquote><p>控制程序决定了数据包处理阶段的具体顺序,即数据包在不同匹配表中间的跳转关系。当表和动作被定义和实现之后,还需要控制程序来确定不同表之间的控制流。P4的控制流包括用于数据处理的表、判决条件以及条件成立时所需采取的操作等组件。</p><p>下图显示了在边缘交换机上<strong>mTag</strong>包处理逻辑示例的控制流:<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/SDN/Language/P4/origin-background/control-flow-for-mTag.png" alt="image"></p><p>被解析(<strong>parser mTag</strong>)之后的数据包,先进入<strong>source_check</strong>表,验证接收到的包和进入端口(Ingress Port)是否和表中的匹配要求一致,即数据包是否包含<strong>mTag</strong>头部,进入端口是否与核心交换机相连。根据该表中的<strong>reads</strong>属性匹配到对应数据包后,由<strong>action</strong>属性指定要采取的动作是:<strong>strip_tag</strong>,即将<strong>mTag</strong>头部从数据包中剥落,并将该数据包是否包含<strong>mTag</strong>头部记录在元数据中,流水线后部分的表可能还会匹配到该元数据,从而避免再次给该数据包打上<strong>mTag</strong>。</p><p>之后<strong>local_switching</strong>表被执行,如果该表未能成功匹配,发生了<strong>misses</strong>,则表示该数据包的目的地不是一个与本交换机相连的主机,这时候就需要执行<strong>mTag_table</strong>。而无论本地局部转发(边缘交换机)还是核心转发(核心交换机),控制流都会进入<strong>egress_check</strong>表,用于处理发往未知目的地的数据包,它会向SDN控制栈发送一个通知。</p><p>综上,可以将数据包处理流水线表示成如下P4程序:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">control <span class="title">main</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="comment">// Verify mTag state and port are consistent</span></span><br><span class="line"> table(source_check);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// If no error from source_check, continue</span></span><br><span class="line"> <span class="keyword">if</span>(!defined(metadata.ingress_error)) {</span><br><span class="line"> <span class="comment">// Attempt to switch to end hosts</span></span><br><span class="line"> table(local_switching);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span>(!defined(metadata.egress_spec)) {</span><br><span class="line"> <span class="comment">// Not a known local host; try mtagging</span></span><br><span class="line"> table(mTag_table);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Check for unknown egress state pr</span></span><br><span class="line"> <span class="comment">// bad retagging with mTag</span></span><br><span class="line"> table(egress_check);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="编译器-Compiler"><a href="#编译器-Compiler" class="headerlink" title="编译器(Compiler)"></a>编译器(Compiler)</h3><p>编写P4程序的五个基本组件后,接下来就需要使用编译器将程序编译并写入到交换机中,其主要分为数据解析逻辑的编译写入和控制流程编译写入。</p><p>数据解析部分用于将网络字节流解析为对应的协议报文,并将报文送到接下来的控制流程中进行匹配和处理。</p><p>控制流程的编译和写入则分为以下两步:</p><ol><li>将P4程序编译,生成设备无关的表依赖图(<strong>Table Dependency Graph, TDG</strong>);</li><li>根据特定的底层转发设备的资源和能力,将表依赖图映射到转发设备的资源上。</li></ol><p>目前P4程序可在软件交换机、拥有RAM和TCAM存储设备的硬件交换机、支持并行表处理的交换机、支持在流水线最后阶段才执行动作的交换机以及拥有少量表资源的交换机等多种交换设备上实现。</p><h3 id="工作流-Work-Flow"><a href="#工作流-Work-Flow" class="headerlink" title="工作流(Work Flow)"></a>工作流(Work Flow)</h3><ol><li>数据包到来后,首先进入可编程定制(<strong>Programmable</strong>)的解析器,用于实现自定义的数据解析流程(针对头部字段,可将网络字节流解析成对应的协议数据包;</li><li><p>数据包解析完毕后是与OpenFlow类似的匹配-动作操作,其流水线(<strong>Pipeline</strong>)支持串行和并行两种模式。受OpenFlow 1.4启发,P4设计的匹配过程也分为入口流水线(<strong>Ingress Pipeline</strong>)和出口流水线(<strong>Egress Pipeline</strong>)两个分离的数据处理流水线;</p></li><li><p>在定义交换机的处理逻辑时,需要定义数据包处理的依赖关系(<strong>Dependency</strong>),即数据包头部字段之间的依赖关系,比如要处理IPv4头部字段,可能需要依赖于以太网头部字段的处理。这些依赖关系可以通过P4描述出来,并编译生成表依赖图TDG,其中每个表都是对应的一种协议或者一个类别的数据包的处理。TDG描述了匹配表之间的逻辑关系,输入和对应操作等行为,用于指导交换机进行数据处理。TDG被定义出来之后,将被编译器翻译成交换机所能理解的逻辑(机器指令),并写入到交换机等交换实体中去,从而完成自定义的数据包处理流程。<br><img src="https://raw.githubusercontent.com/sundongxu/blog-img-hosting/master/images/Network/SDN/Language/P4/origin-background/table-dependency-graph.png" alt="image"></p></li></ol><h2 id="发展趋势"><a href="#发展趋势" class="headerlink" title="发展趋势"></a>发展趋势</h2><hr><p>下面一段话引自原作者<a href="http://www.muzixing.com" target="_blank" rel="noopener"><strong>李呈</strong></a>学长,作为正欲入门SDN的小白,正准备拜读其书<a href="http://item.jd.com/12160066.html" target="_blank" rel="noopener"><strong>《重构网络:SDN架构与实现》</strong></a>,就顺便安利一下吧~</p><blockquote><p>OpenFlow协议目前的框架设计使得OpenFlow无法对转发设备的数据解析以及处理流程进行编程实现,缺少足够的可编程能力。此外,由于OpenFlow的匹配项均为协议相关的,使得每增加一个匹配域均需要对协议栈以及交换机处理流程进行重新编程,而这个过程周期很长,为支持新的OpenFlow协议需要对现有交换机进行升级或者推出新的交换机产品。这样的缺点让OpenFlow协议版本难以稳定,也难以推广。服务提供商在建设网络基础设施时,需要考虑支持OpenFlow什么版本,也要担心未来OpenFlow协议推出新版本时的兼容和设备升级等问题,使得OpenFlow迟迟无法大规模应用。面对OpenFlow的缺陷,P4的推出刚好解决了这个难题。</p><p>P4语言支持对交换机处理逻辑进行编程定义,从而使得协议版本在更新迭代时无需购买新设备,只需通过控制器编程更新交换机处理逻辑即可。这种创新解决了OpenFlow编程能力不足,版本不稳定的问题。此外,由于P4可以编程定义交换机处理逻辑,从而使得交换机可以实现协议无关的转发,进而使得底层交换机更加白盒化,适用范围更广,更容易降低设备采购成本。而且作为一门编程语言,P4支持设备无关特性,使得P4可以应用在不同厂家生产的转发设备上,解除了服务提供商对网络设备厂家绑定的顾虑。</p><p>自P4诞生以来,得到了业界的关注和认可,目前发展良好。作为一门网络编程语言,其大大简化了网络编程的难度,同时也改善了目前SDN可编程能力不足的问题。P4的主要推动者Nick教授是当下SDN最流行的南向协议OpenFlow协议的发明者之一,Jennifer教授也在网络界的先驱。无论是处于对P4技术本身的认同,还是对Nick教授和Jennifer教授的认同,业界,尤其是学术界都对P4的应用前景十分看好,认为其将成为OpenFlow2.0的可能发展方向。目前,P4组织已经有了非常多的成员,其中不乏AT&T、思科、华为、Intel、腾讯和微软等知名企业以及斯坦福大学,普林斯顿大学和康奈尔大学等多个全球顶尖的学术机构。此外,在P4发展的过程中,已经被多种转发设备支持,比如应用最广泛的软件交换机<a href="http://openvswitch.org/" target="_blank" rel="noopener"><strong>OpenVSwitch</strong></a>以及华为的POF交换机。转发设备硬件厂商的大力支持是P4继续发展的重要保障,也是P4商业发展的大前提。</p><p>P4的设计和华为提出的POF十分相似,只不过侧重点和实现方式不同。POF通过{offset,length}来确定数据,强调协议无关,强调指令集,而P4不仅有底层的高度抽象的协议无关指令集,更侧重与控制器端的网络编程语言的构建。还有一点不同的是,同作为开创式的技术,由美国Nick教授等业界先驱推动的P4明显要比由华为提出的POF受到的关注要多,业界对P4的认同也要比POF要高。</p><p>P4和POF相同之处在于:作为完全可编程的SDN实现,性能问题是两者需要面临的大问题,也是急需解决的技术难题。而商业因素方面,两者皆会打破目前的网络界生态平衡。选择搭上这个技术发展的顺风车并争取在新的技术领域占据有利地位,还是固守已有行业市场是网络厂商面连的艰难选择。完全可编程SDN的出现,将使网络的重点和热点进一步由硬件转向软件领域,从而使得依靠硬件技术壁垒占据市场有利地位的传统巨头的优势受到严重削减。虽然巨头的决策将很大程度上影响这些创新技术的发展,但是技术必然会朝着更好更优的方向发展,无论是P4还是POF,抑或是其他的解决方案,具有更好可编程性的SDN就在不远的未来。正如SDN的出现一般,是技术发展过程中顺势而为的产物,是不可阻挡的潮流。</p></blockquote><p>最后送上总结:<br><strong>SDN是未来,P4也势必将在未来的可编程SDN领域大有作为!</strong></p><h2 id="参考资源"><a href="#参考资源" class="headerlink" title="参考资源"></a>参考资源</h2><hr><p>[1] GitHub开源项目<a href="https://github.com/p4lang" target="_blank" rel="noopener"><strong>p4lang</strong></a><br>[2] 李呈前辈<a href="http://www.muzixing.com/pages/2016/03/23/p4zhen-zheng-de-sdnhuan-yao-yuan-ma.html" target="_blank" rel="noopener"><strong>博文</strong></a></p>]]></content>
<categories>
<category> Network </category>
</categories>
<tags>
<tag> P4 </tag>
<tag> OpenFlow </tag>
<tag> SDN </tag>
<tag> Network Measurement </tag>
</tags>
</entry>
</search>