-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.xml
2003 lines (966 loc) · 879 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
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>重拾 MySQL</title>
<link href="/posts/48304.html"/>
<url>/posts/48304.html</url>
<content type="html"><![CDATA[<h3 id="常规"><a href="#常规" class="headerlink" title="常规"></a>常规</h3><ol><li><p>判断是否为空,应该使用 IS NOT null,而不是 <> null</p></li><li><p>null + 非空 = null</p></li><li><p>分组前用 where, 分组后用 having</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">select</span> <span class="built_in">avg</span>(sal) <span class="keyword">as</span> avg_sal, deptno <span class="keyword">from</span> emp <span class="keyword">group</span> <span class="keyword">by</span> dept_no <span class="keyword">having</span> avg_sal <span class="operator"><</span> <span class="number">2000</span>;</span><br></pre></td></tr></table></figure></li><li><p>FROM DUAL DUAL 是一张虚拟表,可以看成占位符</p></li><li><p>查询第三个字母为 O </p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> ename, sal <span class="keyword">FROM</span> emp <span class="keyword">WHERE</span> ename <span class="keyword">LIKE</span> <span class="string">'__O%'</span>;</span><br></pre></td></tr></table></figure></li><li><p>如果 select 语句同时包含有group by, having, limit, order by,那么他们的顺序是group by, having, order by, limit</p></li><li><p>自连接的含义</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> worker.ename <span class="keyword">AS</span> <span class="string">'职员名'</span>, boss.ename <span class="keyword">AS</span> <span class="string">'上级名'</span> <span class="keyword">FROM</span> emp worker, emp boss <span class="keyword">WHERE</span> worker.mgr <span class="operator">=</span> boss.empno;</span><br></pre></td></tr></table></figure></li><li><p>多列子查询</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> emp <span class="keyword">WHERE</span> (deptno, job) <span class="operator">=</span> (<span class="keyword">SELECT</span> deptno, job <span class="keyword">FROM</span> emp <span class="keyword">WHERE</span> ename <span class="operator">=</span> <span class="string">'ALLEN'</span>) <span class="keyword">AND</span> ename <span class="operator">!=</span> <span class="string">'ALLEN'</span>;</span><br></pre></td></tr></table></figure></li><li><p>UNION ALL 查询结果合并,不去重(8)</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> ename, sal, job <span class="keyword">FROM</span> emp <span class="keyword">WHERE</span> sa1<span class="operator">></span><span class="number">2500</span> <span class="comment">--- 5</span></span><br><span class="line"><span class="keyword">UNION</span> <span class="keyword">ALL</span></span><br><span class="line"><span class="keyword">SELECT</span> ename, sal, job <span class="keyword">FROM</span> emp <span class="keyword">WHERE</span> job<span class="operator">=</span><span class="string">'MANAGER'</span> <span class="comment">--- 3</span></span><br></pre></td></tr></table></figure></li><li><p>UNION 查询结果合并去重(3)</p></li></ol> <figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> ename, sal, job <span class="keyword">FROM</span> emp <span class="keyword">WHERE</span> sa1<span class="operator">></span><span class="number">2500</span> <span class="comment">--- 5</span></span><br><span class="line"><span class="keyword">UNION</span></span><br><span class="line"><span class="keyword">SELECT</span> ename, sal, job <span class="keyword">FROM</span> emp <span class="keyword">WHERE</span> job<span class="operator">=</span><span class="string">'MANAGER'</span> <span class="comment">--- 3</span></span><br></pre></td></tr></table></figure><h3 id="索引"><a href="#索引" class="headerlink" title="索引"></a>索引</h3><h4 id="类型"><a href="#类型" class="headerlink" title="类型"></a>类型</h4><ol><li>主键索引:主键自动为主索引</li><li>唯一索引:UNIQUE</li><li>普通索引:INDEX</li><li>全文索引:FULLTEXT,适用于 MyISAM(存储引擎),全文搜索:Solr 和 ElasticSearch</li></ol><h4 id="创建规则"><a href="#创建规则" class="headerlink" title="创建规则"></a>创建规则</h4><ol><li>频繁查询的字段;</li><li>唯一性太差的字段;</li><li>更新非常频繁的字段不适合创建索引;</li><li>不会出现在 WHERE 子句中的字段不该创建索引;</li></ol><h4 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h4><p>查询表是否有索引</p><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">SHOW</span> INDEX <span class="keyword">FROM</span> table_name;</span><br><span class="line"><span class="keyword">SHOW</span> INDEXS <span class="keyword">FROM</span> table_name;</span><br><span class="line"><span class="keyword">SHOW</span> KEYS <span class="keyword">FROM</span> table_name;</span><br></pre></td></tr></table></figure><h3 id="事务"><a href="#事务" class="headerlink" title="事务"></a>事务</h3><h4 id="概念"><a href="#概念" class="headerlink" title="概念"></a>概念</h4><p>用于保证数据的一致性,由一组相关的 DML 语句组成。组中的语句要么全都成功,要么全都失败。应用场景:转账。。。</p><h4 id="操作"><a href="#操作" class="headerlink" title="操作"></a>操作</h4><figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">start</span> transaction</span><br><span class="line"><span class="keyword">savepoint</span> 保存点名</span><br><span class="line"><span class="keyword">rollback</span> <span class="keyword">to</span> 保存点名</span><br><span class="line"><span class="keyword">rollback</span> ——回退所有事务</span><br><span class="line"><span class="keyword">commit</span></span><br></pre></td></tr></table></figure><ul><li>使用 commit 语句提交结束事务之后,其他会话将可以看到变化后的新数据。</li><li>在不显式开启事务的情况下, DML 操作是自动提交的,不能回滚</li></ul><h3 id="事务隔离级别"><a href="#事务隔离级别" class="headerlink" title="事务隔离级别"></a>事务隔离级别</h3><ul><li>脏读:当一个事务读取另一个事务<strong>尚未提交的语句</strong>时,产生脏读</li><li>不可重复读:同一查询在同一事务中多次进行,由于其他提交事务所做的<strong>修改或删除</strong>,每次返回不同的结果集,此时发生不可重复读。</li><li>幻读:同一查询在同一事务中多次进行,由于其他提交事务所做的<strong>插入操作</strong>,每次返回不同的结果集,此时发生幻读。</li></ul><p><img src="https://images.961110.xyz:5001/i/2022/12/05/mysql_isolation_level.png" alt="image-20221205230217639"></p><p>默认隔离级别:可重复读</p><h3 id="函数"><a href="#函数" class="headerlink" title="函数"></a>函数</h3><p><img src="https://images.961110.xyz:5001/i/2022/12/01/str_func.png" alt="image-20221201231417724"></p><p><img src="https://images.961110.xyz:5001/i/2022/12/01/math_func.png" alt="image-20221201232053347"></p><p><img src="https://images.961110.xyz:5001/i/2022/12/01/date_func.png" alt="image-20221201232348525"></p><p><img src="https://images.961110.xyz:5001/i/2022/12/01/process_control_func.png" alt="image-20221201233745857"></p>]]></content>
<categories>
<category> 数据库 </category>
</categories>
<tags>
<tag> 数据库 </tag>
</tags>
</entry>
<entry>
<title>重拾 Java 系列 —— 其他</title>
<link href="/posts/2290.html"/>
<url>/posts/2290.html</url>
<content type="html"><![CDATA[<h2 id="重拾-Java-系列-——-其他"><a href="#重拾-Java-系列-——-其他" class="headerlink" title="重拾 Java 系列 —— 其他"></a>重拾 Java 系列 —— 其他</h2><h3 id="JDK-工具"><a href="#JDK-工具" class="headerlink" title="JDK 工具"></a>JDK 工具</h3><h4 id="javap"><a href="#javap" class="headerlink" title="javap"></a>javap</h4><p>JDK 提供的命令行工具,反编译 class 字节码文件</p><h3 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h3><p>三元运算符要看作整体</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">Objdect obj = <span class="keyword">true</span>? <span class="keyword">new</span> Interge(<span class="number">1</span>): <span class="keyword">new</span> Double(<span class="number">2.0</span>);</span><br><span class="line">System.out.println(obj); <span class="comment">// 1.0(自动提升精度)</span></span><br></pre></td></tr></table></figure><h3 id="反射"><a href="#反射" class="headerlink" title="反射"></a>反射</h3><h4 id="类加载流程图"><a href="#类加载流程图" class="headerlink" title="类加载流程图"></a>类加载流程图</h4><p><img src="https://images.961110.xyz:5001/i/2022/12/17/class_loading_process.png" alt="image-20221217205330201"></p><h4 id="加载阶段"><a href="#加载阶段" class="headerlink" title="加载阶段"></a>加载阶段</h4><p>JVM 将字节码从不同数据源(可能是 class 文件,可能是 jar 包,也可能来源于网络)转化为二进制字节流加载到内存(方法区)中,并生成一个代表该类的 java.lang.Class 对象(堆中)。</p><h4 id="连接阶段——验证"><a href="#连接阶段——验证" class="headerlink" title="连接阶段——验证"></a>连接阶段——验证</h4><ul><li>目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。<ul><li>在类加载过程中会创建一个 securityManager 进行验证工作,包括:文件格式验证(是否以魔数 0xcafebabe 开头,固定格式)、元数据验证、字节码验证和符号引用验证</li></ul></li><li>可以考虑使用<code>-Xverify:none</code>参数来关闭大部分的类验证措施,缩短虚拟机类加载的时间。</li></ul><h4 id="连接阶段——准备"><a href="#连接阶段——准备" class="headerlink" title="连接阶段——准备"></a>连接阶段——准备</h4><p>JVM 对<strong>静态变量</strong>分配内存并进行默认初始化(0 0L null false 等),分配的内存位于方法区。</p><p> <img src="https://images.961110.xyz:5001/i/2022/12/17/static_varibale.png" alt="image-20221217211238620"></p><h4 id="连接阶段——解析"><a href="#连接阶段——解析" class="headerlink" title="连接阶段——解析"></a>连接阶段——解析</h4><p>JVM 将常量池内的符号引用替换为直接引用(内存地址)</p><h4 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h4><p>真正开始执行类中定义的 Java 代码。</p><p>该阶段是执行<code><clinit>()</code>方法的阶段</p><ul><li><code><clinit>()</code>方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并。<ul><li>同一个静态变量赋值两次,以最后一个为准</li></ul></li><li>虚拟机会保证一个类的<code><clinit>()</code>方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<code><clinit>()</code>方法,其他线程都需要阻塞等待,直到活动线程执行<code><clinit>()</code>方法完毕。</li></ul>]]></content>
<categories>
<category> Java </category>
</categories>
<tags>
<tag> Java </tag>
</tags>
</entry>
<entry>
<title>重拾 Java 系列 —— 面向对象</title>
<link href="/posts/30000.html"/>
<url>/posts/30000.html</url>
<content type="html"><![CDATA[<h2 id="重拾-Java-系列-——-面向对象"><a href="#重拾-Java-系列-——-面向对象" class="headerlink" title="重拾 Java 系列 —— 面向对象"></a>重拾 Java 系列 —— 面向对象</h2><h3 id="类"><a href="#类" class="headerlink" title="类"></a>类</h3><h4 id="类和对象的内存分配机制"><a href="#类和对象的内存分配机制" class="headerlink" title="类和对象的内存分配机制"></a>类和对象的内存分配机制</h4><ol><li>栈:一般存放基本数据类型(局部变量)</li><li>堆:存放对象(自定义类、数组等)</li><li>方法区:常量池(常量,比如字符串常量)、类加载信息</li></ol><h4 id="方法"><a href="#方法" class="headerlink" title="方法"></a>方法</h4><ol><li><p>Java 中允许方法重载,方法名相同,形参列表不同(类型或个数或顺序),返回值类型无要求</p></li><li><p>可变参数的实参可以为数组,其本质就是数组</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">int</span>[] arr = {<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>};</span><br><span class="line">sum(arr);</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">sum</span><span class="params">(<span class="keyword">int</span>... nums)</span></span>{</span><br><span class="line"> }</span><br></pre></td></tr></table></figure></li></ol><h4 id="构造方法-构造器"><a href="#构造方法-构造器" class="headerlink" title="构造方法/构造器"></a>构造方法/构造器</h4><p>构造方法的主要作用是完成新对象的初始化,并不是创建对象。</p><ol><li>构造方法没有返回值;</li><li>方法名必须和类名字一样;</li><li>构造方法由系统自动调用;</li><li>构造方法可以重载;</li><li>没有显式定义构造方法,系统会生成默认无参构造方法。一旦显式定义了构造方法,默认构造方法就被覆盖;</li></ol><h4 id="Java-创建对象的流程"><a href="#Java-创建对象的流程" class="headerlink" title="Java 创建对象的流程"></a>Java 创建对象的流程</h4><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Person</span></span>{</span><br><span class="line"> <span class="keyword">int</span> age = <span class="number">90</span>;</span><br><span class="line"> String name;</span><br><span class="line"> Person(String n, <span class="keyword">int</span> a){</span><br><span class="line"> name = n;</span><br><span class="line"> age = a;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line">Person p = <span class="keyword">new</span> Person(<span class="string">"小倩"</span>, <span class="number">20</span>);</span><br></pre></td></tr></table></figure><ol><li>先在方法区加载类信息(属性和方法信息,只会加载一次)</li><li>在堆中给对象分配空间;</li><li>完成对象初始化<ol><li>默认值初始化(<code>int = 0 string = null</code>);</li><li>显式初始化;</li><li>使用构造器进行初始化;</li></ol></li><li>把地址赋给类的实例;</li></ol><h4 id="this-指针"><a href="#this-指针" class="headerlink" title="this 指针"></a>this 指针</h4><p>this 指向类实例的地址</p><p><code>this(参数列表)</code> 可以直接访问构造器(只能在构造器中访问另一个构造器,且必须放在第一条语句)</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">T</span></span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">T</span><span class="params">()</span></span>{</span><br><span class="line"> <span class="keyword">this</span>(<span class="string">"jack"</span>,<span class="number">9</span>);</span><br><span class="line"> System.out.println(<span class="string">"T() 构造器"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">T</span><span class="params">(String name, <span class="keyword">int</span> age)</span></span>{</span><br><span class="line"> System.out.println(<span class="string">" T(String name, int age) 构造器"</span>);</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>命名规范:com.公司名.项目名.业务模块名 com.alibaba.oa.model; com.alibaba.oa.controller</p><p>package 只能放在一个文件的第一行</p><h4 id="访问修饰符"><a href="#访问修饰符" class="headerlink" title="访问修饰符"></a>访问修饰符</h4><ul><li>public: 同类、同包、子类、不同包都可以访问;</li><li>protected: 同类、同包、子类都可以访问,不同包不能访问;</li><li>private: 同类可以访问,同包、子类、不同包均不能访问;</li><li>没有修饰符:同类、同包都可以访问,子类、不同包不能访问;</li></ul><p>只有 默认 和 public 可以修饰类</p><h3 id="继承"><a href="#继承" class="headerlink" title="继承"></a>继承</h3><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Sub</span> <span class="keyword">extends</span> <span class="title">Base</span></span>{</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol><li>非私有属性和方法可以在子类直接访问</li><li>子类必须调用父类的构造器,完成父类的初始化。</li><li>不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器。如果父类没有提供无参构造器,则必须在子类的构造器中用<code>super</code>去指定使用父类的哪个构造器。</li><li><code>super</code>在使用时,需要放置在构造器第一行</li><li>Java 所有类都是<code>Object</code>类的子类,父类构造器的调用一直往上追溯到<code>Object</code>类</li><li>Java 是单继承机制</li></ol><h4 id="本质"><a href="#本质" class="headerlink" title="本质"></a>本质</h4><p>先加载顶级类,再逐层加载,最后加载<code>Son</code></p><p><img src="https://images.961110.xyz:5001/i/2022/11/23/image-20221123232237272.png" alt="image-20221123232237272"></p><h4 id="super"><a href="#super" class="headerlink" title="super"></a>super</h4><p><code>super</code>代表父类的引用,用于访问父类的属性、方法、构造器。</p><p>分工明确:父类属性由父类初始化,子类属性由子类初始化</p><p>子类和父类中的属性、方法没有重名时,使用<code>super</code>、<code>this</code>、直接访问都是一样的效果</p><p>多个上级类由同名的成员时,<code>super</code>遵循就近原则,同时需要遵守访问权限的规则<img src="https://images.961110.xyz:5001/i/2022/11/23/super--this-.png" alt="image-20221123233430891"></p><h3 id="接口"><a href="#接口" class="headerlink" title="接口"></a>接口</h3><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">interface</span> 接口名</span>{</span><br><span class="line"> <span class="comment">// 属性</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">int</span> n = <span class="number">10</span>;</span><br><span class="line"> <span class="comment">// 方法(抽象方法、默认实现方法、静态方法)</span></span><br><span class="line"> <span class="comment">// 抽象方法可以省略 abstract 关键字</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">hi</span><span class="params">()</span></span>;</span><br><span class="line"> <span class="function"><span class="keyword">default</span> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title">ok</span><span class="params">()</span></span>{</span><br><span class="line"> ...;</span><br><span class="line"> }</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">cry</span><span class="params">()</span></span>{</span><br><span class="line"> ...;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li>接口不能被实例化</li><li>接口中所有的方法都是 public 的</li><li>接口中抽象方法可以省略 abstract 关键字</li><li>一个普通类实现接口,必须实现接口的所有方法</li><li>抽象类实现接口,可以不用实现接口的方法</li><li>一个类可以同时实现多个接口</li><li>接口的属性,默认是 public static final 的,必须初始化</li><li>接口不能继承其他类,但是可以继承多个别的接口</li></ul><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">USBInterface</span></span>{</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">start</span><span class="params">()</span></span>;</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">stop</span><span class="params">()</span></span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Camera</span> <span class="keyword">implements</span> <span class="title">USBInterface</span></span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">start</span><span class="params">()</span></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">public</span> <span class="keyword">void</span> <span class="title">stop</span><span class="params">()</span></span>{</span><br><span class="line"> ...;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Computer</span></span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">work</span><span class="params">(USBInterface usbInterface)</span></span>{</span><br><span class="line"> usbInterface.start();</span><br><span class="line"> usbInterface.stop();</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><p>注解(Annotation)也被称为元数据(metadata),用于修饰包、类、方法、属性、构造器、局部变量等信息</p><h3 id="异常"><a href="#异常" class="headerlink" title="异常"></a>异常</h3><p>子类抛出的异常类型应该和父类抛出的异常保持一致或是其子类</p><h3 id="包装类"><a href="#包装类" class="headerlink" title="包装类"></a>包装类</h3><p>所有包装类都是不可继承的</p><p><img src="https://images.961110.xyz:5001/i/2022/12/07/wrapper_class.png" alt="image-20221207222520140"></p><ul><li>装箱:基本类型->包装类型</li><li>拆箱:基本类型<-包装类型</li></ul><p>自动装箱底层调用的是 <code>valueOf()</code> 方法</p><h4 id="Integer"><a href="#Integer" class="headerlink" title="Integer"></a>Integer</h4><figure class="highlight java"><table><tr><td class="code"><pre><span class="line">Integer i= <span class="number">100</span>;</span><br><span class="line">String str1 = i+<span class="string">""</span>; <span class="comment">// "100"</span></span><br><span class="line">String str2 = i.toString(); <span class="comment">// "100"</span></span><br><span class="line">String str3 = String.valueOf(i); <span class="comment">// "100"</span></span><br><span class="line"></span><br><span class="line">String str4 = <span class="string">"12345"</span>;</span><br><span class="line">Integer i2 = Integer.parseInt(str4);</span><br><span class="line">Integer i3 = <span class="keyword">new</span> Integer(str4);</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>{</span><br><span class="line"> Integer i = <span class="keyword">new</span> Integer(<span class="number">1</span>);</span><br><span class="line"> Integer j = <span class="keyword">new</span> Integer(<span class="number">1</span>);</span><br><span class="line"> System.out.println(i == j); <span class="comment">//False</span></span><br><span class="line"> <span class="comment">/*</span></span><br><span class="line"><span class="comment"> //1. 如果i 在 IntegerCache.low(-128)~IntegerCache.high(127),就直接从数组返回</span></span><br><span class="line"><span class="comment"> //2. 如果不在 -128~127,就直接 new Integer(i)</span></span><br><span class="line"><span class="comment"> public static Integer valueOf(int i) {</span></span><br><span class="line"><span class="comment"> if (i >= IntegerCache.low && i <= IntegerCache.high)</span></span><br><span class="line"><span class="comment"> return IntegerCache.cache[i + (-IntegerCache.low)];</span></span><br><span class="line"><span class="comment"> return new Integer(i);</span></span><br><span class="line"><span class="comment"> }</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> Integer m = <span class="number">1</span>; <span class="comment">//底层 Integer.valueOf(1); -> 阅读源码</span></span><br><span class="line"> Integer n = <span class="number">1</span>;<span class="comment">//底层 Integer.valueOf(1);</span></span><br><span class="line"> System.out.println(m == n); <span class="comment">//T</span></span><br><span class="line"> <span class="comment">//所以,这里主要是看范围 -128 ~ 127 就是直接返回</span></span><br><span class="line"> <span class="comment">//,否则,就new Integer(xx);</span></span><br><span class="line"> Integer x = <span class="number">128</span>;<span class="comment">//底层Integer.valueOf(1);</span></span><br><span class="line"> Integer y = <span class="number">128</span>;<span class="comment">//底层Integer.valueOf(1);</span></span><br><span class="line"> System.out.println(x == y);<span class="comment">//False</span></span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="String"><a href="#String" class="headerlink" title="String"></a>String</h4><ol><li>String 不能被其他类继承</li><li>String 有属性<code>private final char Value []</code>用于存放字符串内容<br><code>value</code>不可修改,即<code>value</code>不能指向新的地址,但是单个字符内容是可以变化</li></ol><p>String 两种构造方法的不同内存布局</p><p>方式一:先从常量池查看是否有”hsp”数据空间,如果有则直接指向;如果<br>没有则重新创建,然后指向。<code>s</code>最终指向的是常量池的空间地址<br>方式二:先在堆中创建空间,里面维护了value属性,指向常量池的hsp空间。<br>如果常量池没有”hsp”,重新创建,如果有则直接通过value指向。最终指向<br>的是堆中的空间地址。</p><p><img src="https://images.961110.xyz:5001/i/2022/12/07/string_create.png" alt="image-20221208001347768"></p><ul><li>String c = “ab”+”cd” c 指向的是常量池中 “abcd” 的地址</li><li>String d = a + b,实际是在新建了一个 String 对象。</li><li>String str = “hello” 取”h”不能用 str[0]</li></ul><h4 id="StringBuffer"><a href="#StringBuffer" class="headerlink" title="StringBuffer"></a>StringBuffer</h4><ul><li>StringBuffer是可变字符序列,长度可变,是一个容器。</li><li>final 类,不可被继承</li><li>在父类 AbstractStringBuilder 中有属性 char[] value,存放字符串内容,可以被修改。</li><li>StringBuffer 的修改不用每次都更新地址,效率更高</li><li>默认构造器中初始容量设定为 16 个字符</li><li>线程安全</li></ul><h4 id="StringBuilder"><a href="#StringBuilder" class="headerlink" title="StringBuilder"></a>StringBuilder</h4><ul><li>可变字符序列,类似 StringBuffer,不同的是 StringBuilder 不是线程安全的。在单线程下,性能比 StringBuffer 好。</li><li>final 类</li></ul><h3 id="集合"><a href="#集合" class="headerlink" title="集合"></a>集合</h3><p><img src="https://images.961110.xyz:5001/i/2022/12/08/collection.png" alt="image-20221208231622527"></p><p>实现了 Iterable 接口的,都可以被迭代器迭代</p><p>实现了 List 接口的,集合中元素有序且可以重复</p><p><img src="https://images.961110.xyz:5001/i/2022/12/08/map.png" alt="image-20221208231711219"></p><h4 id="迭代器"><a href="#迭代器" class="headerlink" title="迭代器"></a>迭代器</h4><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">CollectionIterator</span> </span>{</span><br><span class="line"> <span class="meta">@SuppressWarnings({"all"})</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>{</span><br><span class="line"></span><br><span class="line"> Collection col = <span class="keyword">new</span> ArrayList();</span><br><span class="line"></span><br><span class="line"> col.add(<span class="keyword">new</span> Book(<span class="string">"三国演义"</span>, <span class="string">"罗贯中"</span>, <span class="number">10.1</span>));</span><br><span class="line"> col.add(<span class="keyword">new</span> Book(<span class="string">"小李飞刀"</span>, <span class="string">"古龙"</span>, <span class="number">5.1</span>));</span><br><span class="line"> col.add(<span class="keyword">new</span> Book(<span class="string">"红楼梦"</span>, <span class="string">"曹雪芹"</span>, <span class="number">34.6</span>));</span><br><span class="line"></span><br><span class="line"> Iterator iterator = col.iterator();</span><br><span class="line"> <span class="keyword">while</span> (iterator.hasNext()) {</span><br><span class="line"> Object obj = iterator.next();</span><br><span class="line"> System.out.println(<span class="string">"obj="</span> + obj);</span><br><span class="line"></span><br><span class="line"> }</span><br><span class="line"> <span class="comment">//3. 当退出while循环后 , 这时iterator迭代器,指向最后的元素</span></span><br><span class="line"> <span class="comment">// iterator.next();//NoSuchElementException</span></span><br><span class="line"> <span class="comment">//4. 如果希望再次遍历,需要重置我们的迭代器</span></span><br><span class="line"> iterator = col.iterator();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 增强 for,底层也是 iterator</span></span><br><span class="line"> <span class="keyword">for</span> (Object book: col){</span><br><span class="line"> System.out.println(<span class="string">"obj="</span> + obj);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>]]></content>
<categories>
<category> Java </category>
</categories>
<tags>
<tag> Java </tag>
</tags>
</entry>
<entry>
<title>重拾 Java 系列 —— 基础</title>
<link href="/posts/13522.html"/>
<url>/posts/13522.html</url>
<content type="html"><![CDATA[<h2 id="重拾-Java-系列-——-杂七杂八"><a href="#重拾-Java-系列-——-杂七杂八" class="headerlink" title="重拾 Java 系列 —— 杂七杂八"></a>重拾 Java 系列 —— 杂七杂八</h2><h3 id="细节"><a href="#细节" class="headerlink" title="细节"></a>细节</h3><ol><li>一个源文件中<strong>最多有一个 public 类</strong>;</li><li>如果一个源文件包含一个 public 类,则<strong>文件名必须按该类命名</strong>;</li><li><code>\r</code> 回车:回到当前行首 <code>\n</code> 换行:另起一行;</li></ol><h4 id="注释"><a href="#注释" class="headerlink" title="注释"></a>注释</h4><ol><li><p>多行注释不能嵌套;</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">comments</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line">*/</span><br></pre></td></tr></table></figure></li><li><p>文档注释</p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> JasonChio</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@version</span> 1.0</span></span><br><span class="line"><span class="comment"> */</span></span><br></pre></td></tr></table></figure></li></ol><h4 id="规范"><a href="#规范" class="headerlink" title="规范"></a>规范</h4><ol><li>源文件使用 utf-8 编码,大小可变(字母用一个字节,汉字用三个字节)</li></ol><h4 id="基本数据类型"><a href="#基本数据类型" class="headerlink" title="基本数据类型"></a>基本数据类型</h4><ol><li><p>默认浮点类型为<code>float</code></p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">float</span> = <span class="number">1.1</span>; <span class="comment">// 错误,1.1 为 double 类型</span></span><br><span class="line"><span class="keyword">float</span> = <span class="number">1.1f</span>; <span class="comment">// 正确</span></span><br></pre></td></tr></table></figure></li><li><p><strong>浮点数运算注意事项</strong></p><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="keyword">double</span> num11 = <span class="number">2.7</span>;</span><br><span class="line"><span class="keyword">double</span> num12 = <span class="number">8.1</span> / <span class="number">3</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 在比较两个浮点数时,通过其差值的绝对值来判断</span></span><br><span class="line"><span class="keyword">if</span> (Math.abs(num11 - num12 ) < <span class="number">0.000001</span> ){</span><br><span class="line"> System.out.println(<span class="string">""</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li><li><p>char 类型赋值用单引号,双引号的含义是字符串</p></li><li><p>char 的本质是一个整数,输出的是 unicode 对应的字符,可以进行运算</p></li><li><p>String 的默认是为 null</p></li></ol><h4 id="基本数据类型转换"><a href="#基本数据类型转换" class="headerlink" title="基本数据类型转换"></a>基本数据类型转换</h4><ol><li><p>进行复制或者预算时,精度小的类型自动转为精度大的类型</p><p>byte - short - int - long - float - double</p></li><li><p>```<br>int n = 10;<br>float d = n + 1.1 // 报错,1.1 是 double 类型,运算结果也为 double 类型</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line">3. (byte, short) 和 char 之间不会相互自动转换</span><br><span class="line"></span><br><span class="line">4. byte, short, char 三者可以做运算,计算前先转为 int 类型</span><br><span class="line"></span><br><span class="line">#### 强制类型转换</span><br><span class="line"></span><br><span class="line">1. 将容量大的数据类型转为容量小的数据类型,可能会造成精度降低甚至溢出</span><br><span class="line"></span><br><span class="line">2. 强转符号只针对最近的操作数有效</span><br><span class="line"></span><br><span class="line"> ```java</span><br><span class="line"> int x = (int)10 * 3.5 + 6 * 1.5;// 报错</span><br></pre></td></tr></table></figure></li></ol><h4 id="位运算"><a href="#位运算" class="headerlink" title="位运算"></a>位运算</h4><ol><li>算术右移<code>>></code>: 低位溢出(丢弃),符号位不变,并用符号位补溢出的高位</li><li>算术左移<code><<</code>: 符号位不变,低位补 0</li><li><code>>>></code>逻辑右移也叫无符号右移,运算规则是:低位溢出,高位补 0</li><li>特别说明:没有<code><<<</code>符号</li></ol><h4 id="分支结构"><a href="#分支结构" class="headerlink" title="分支结构"></a>分支结构</h4><ol><li>switch 条件表达式的返回值必须是: byte, short, int, char, enum, String</li></ol><h4 id="数组"><a href="#数组" class="headerlink" title="数组"></a>数组</h4><ol><li>数组是引用类型;</li><li>String 类型的数组创建后如果没有赋值,其默认值是 null;</li><li>数组在默认情况下是引用传递,赋的值是地址。数组复制注意要深拷贝。</li></ol><h4 id="作用域"><a href="#作用域" class="headerlink" title="作用域"></a>作用域</h4><ol><li>全局变量有默认值,局部变量没有默认值,必须赋值后使用;</li></ol><h3 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h3><ol><li><a href="https://www.bilibili.com/video/BV1fh411y7R8">零基础30天学会Java</a></li></ol>]]></content>
<categories>
<category> Java </category>
</categories>
<tags>
<tag> Java </tag>
</tags>
</entry>
<entry>
<title>【转载】Go init 顺序</title>
<link href="/posts/16643.html"/>
<url>/posts/16643.html</url>
<content type="html"><![CDATA[<h2 id="Go-程序初始化顺序"><a href="#Go-程序初始化顺序" class="headerlink" title="Go 程序初始化顺序"></a>Go 程序初始化顺序</h2><p>Go 程序的初始化和执行总是从<code>main.main</code>函数开始的。但是如果<code>main</code>包里导入了其它的包,则会按照顺序将它们包含进<code>main</code>包里(这里的导入顺序依赖具体实现,一般可能是以文件名或包路径名的字符串顺序导入)。如果某个包被多次导入的话,在执行的时候只会导入一次。当一个包被导入时,如果它还导入了其它的包,则先将其它的包包含进来,然后创建和初始化这个包的常量和变量。然后就是调用包里的<code>init</code>函数,如果一个包有多个<code>init</code>函数的话,实现可能是以文件名的顺序调用,同一个文件内的多个<code>init</code>则是以出现的顺序依次调用(<code>init</code>不是普通函数,可以定义有多个,所以不能被其它函数调用)。最终,在<code>main</code>包的所有包常量、包变量被创建和初始化,并且<code>init</code>函数被执行后,才会进入<code>main.main</code>函数,程序开始正常执行。下图是 Go 程序函数启动顺序的示意图:</p><p><img src="https://images.961110.xyz:5001/i/2022/10/09/go_init_order.png"></p><p>要注意的是,在<code>main.main</code>函数执行之前所有代码都运行在同一个Goroutine中,也是运行在程序的主系统线程中。如果某个<code>init</code>函数内部用go关键字启动了新的Goroutine的话,新的Goroutine和<code>main.main</code>函数是并发执行的。</p><p>因为所有的<code>init</code>函数和<code>main</code>函数都是在主线程完成,它们也是满足顺序一致性模型的。</p><h3 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h3><ol><li><a href="https://chai2010.gitbooks.io/advanced-go-programming-book/content/ch1-basic/ch1-05-mem.html">1.5 面向并发的内存模型 · Go语言高级编程 (gitbooks.io)</a></li></ol>]]></content>
<categories>
<category> 代码 </category>
</categories>
<tags>
<tag> Go </tag>
</tags>
</entry>
<entry>
<title>Go 定时任务执行时间大于定时器间隔</title>
<link href="/posts/36855.html"/>
<url>/posts/36855.html</url>
<content type="html"><![CDATA[<h3 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h3><p>当<code>time.NewTicker()</code>定时任务执行的时间比<code>ticker</code>设定的时间间隔长时,会发生什么事情?</p><h3 id="验证"><a href="#验证" class="headerlink" title="验证"></a>验证</h3><p>有如下代码</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span>{</span><br><span class="line"> ScheduledTask()</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">SleepTask</span><span class="params">()</span></span>{</span><br><span class="line"> time.Sleep(<span class="number">6</span> * time.Second)</span><br><span class="line"> fmt.Println(<span class="string">"睡醒了"</span>)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ScheduledTask</span><span class="params">()</span></span>{</span><br><span class="line"> ticker := time.NewTicker(time.Second * <span class="number">3</span>)</span><br><span class="line"> fmt.Println(<span class="string">"ScheduledTask start."</span>)</span><br><span class="line"> <span class="keyword">for</span> _ = <span class="keyword">range</span> ticker.C {</span><br><span class="line"> fmt.Println(<span class="string">"三秒到了"</span>)</span><br><span class="line"> SleepTask()</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>运行结果</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="number">2022</span>/<span class="number">08</span>/<span class="number">28</span> <span class="number">21</span>:<span class="number">49</span>:<span class="number">33.293</span> [I] 三秒到了</span><br><span class="line"><span class="number">2022</span>/<span class="number">08</span>/<span class="number">28</span> <span class="number">21</span>:<span class="number">49</span>:<span class="number">39.306</span> [I] 睡醒了</span><br><span class="line"><span class="number">2022</span>/<span class="number">08</span>/<span class="number">28</span> <span class="number">21</span>:<span class="number">49</span>:<span class="number">39.308</span> [I] 三秒到了</span><br><span class="line"><span class="number">2022</span>/<span class="number">08</span>/<span class="number">28</span> <span class="number">21</span>:<span class="number">49</span>:<span class="number">45.309</span> [I] 睡醒了</span><br><span class="line"><span class="number">2022</span>/<span class="number">08</span>/<span class="number">28</span> <span class="number">21</span>:<span class="number">49</span>:<span class="number">45.310</span> [I] 三秒到了</span><br><span class="line"><span class="number">2022</span>/<span class="number">08</span>/<span class="number">28</span> <span class="number">21</span>:<span class="number">49</span>:<span class="number">51.322</span> [I] 睡醒了</span><br><span class="line"><span class="number">2022</span>/<span class="number">08</span>/<span class="number">28</span> <span class="number">21</span>:<span class="number">49</span>:<span class="number">51.323</span> [I] 三秒到了</span><br></pre></td></tr></table></figure><h3 id="答案"><a href="#答案" class="headerlink" title="答案"></a>答案</h3><p>当定时任务执行时间超过定时器设定时间时,定时任务执行结束后会立即执行定时任务。</p>]]></content>
<categories>
<category> 代码 </category>
</categories>
<tags>
<tag> Go </tag>
</tags>
</entry>
<entry>
<title>博客从 GitHub 迁移到个人服务器</title>
<link href="/posts/61140.html"/>
<url>/posts/61140.html</url>
<content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>因为之前的服务器到期了,所以换了一台新的服务器,同时之前的写过的博客需要跟着迁移。</p><p>博客框架选择的是 Hexo,主题是 Butterfly,本教程理论上跟框架和主题无关。</p><h2 id="操作方法"><a href="#操作方法" class="headerlink" title="操作方法"></a>操作方法</h2><h3 id="前置条件(已有请跳过)"><a href="#前置条件(已有请跳过)" class="headerlink" title="前置条件(已有请跳过)"></a>前置条件(已有请跳过)</h3><ol><li>在本地已经搭建好了 Hexo 环境,<a href="https://www.nesxc.com/119/">参考链接</a></li><li>服务器有公网 IP</li><li>安装 Git <code>sudo apt install git</code></li></ol><h3 id="服务器创建博客仓库"><a href="#服务器创建博客仓库" class="headerlink" title="服务器创建博客仓库"></a>服务器创建博客仓库</h3><ol><li><p>创建裸库</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">mkdir /home/jason/Blog</span><br><span class="line"><span class="built_in">cd</span> /home/jason/Blog</span><br><span class="line">git init --bare blog.git</span><br></pre></td></tr></table></figure></li><li><p>创建博客页面存放目录</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">mkdir /home/jason/Blog/hexo</span><br></pre></td></tr></table></figure></li><li><p>创建 Git 钩子</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">vim blog.git/hooks/post-receive</span><br></pre></td></tr></table></figure><p>输入以下内容</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="meta">#!/bin/sh</span></span><br><span class="line">git --work-tree=/home/jason/Blog/hexo --git-dir=/home/jason/Blog/blog.git checkout -f</span><br></pre></td></tr></table></figure><p>给钩子赋予权限</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">chmod +x /home/jason/Blog/blog.git/hooks/post-receive</span><br></pre></td></tr></table></figure></li><li><p>至此,服务器博客的仓库已经创建完毕了。</p></li></ol><h3 id="本地配置"><a href="#本地配置" class="headerlink" title="本地配置"></a>本地配置</h3><ol><li><p>打开位于Hexo博客根目录下的<code>_config.yml</code>文件,找到<code>deploy</code>并增加部署链接</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># Deployment</span></span><br><span class="line"><span class="comment">## Docs: https://hexo.io/docs/one-command-deployment</span></span><br><span class="line">deploy:</span><br><span class="line">- <span class="built_in">type</span>: git</span><br><span class="line"> repo:</span><br><span class="line"> github: https://github.com/jasonchio-cn/jasonchio-cn.github.io</span><br><span class="line"> AliServer: LabAliServer:/home/jason/Blog/blog.git</span><br><span class="line"> branch: master</span><br><span class="line"> message: 更新博客</span><br><span class="line">- <span class="built_in">type</span>: baidu_url_submitter</span><br></pre></td></tr></table></figure><p><code>AliServer</code>是部署端的名字,LabAliServer 是我在本地<code>.ssh</code>文件夹下<code>config</code>文件中配置的</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">Host LabAliServer</span><br><span class="line"> HostName 服务器 IP</span><br><span class="line"> User jason</span><br><span class="line"> IdentityFile C:\Users\JasonChio\.ssh\jason_aliyun.aliyun</span><br></pre></td></tr></table></figure></li><li><p>测试能否推送到服务器博客仓库。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">hexo clean & hexo g && hexo d</span><br></pre></td></tr></table></figure><p><img src="https://images.961110.xyz:5001/i/2022/04/29/blog_repo.png" alt="image-20220429214554004"></p></li></ol><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><ol><li><a href="https://blog.csdn.net/zss192/article/details/105376347">从github迁移hexo博客到服务器</a></li><li><a href="https://blog.51cto.com/u_15400016/4287649#docker_163">记录本地Hexo博客部署到服务器上</a></li></ol>]]></content>
<categories>
<category> 教程 </category>
</categories>
<tags>
<tag> 教程 </tag>
</tags>
</entry>
<entry>
<title>博客美化记录</title>
<link href="/posts/7990.html"/>
<url>/posts/7990.html</url>
<content type="html"><![CDATA[<h2 id="插件"><a href="#插件" class="headerlink" title="插件"></a>插件</h2><h3 id="Gitcalendar"><a href="#Gitcalendar" class="headerlink" title="Gitcalendar"></a>Gitcalendar</h3><p>参考链接:<a href="https://akilar.top/posts/1f9c68c9/">♪(^∇^*)欢迎回来!Gitcalendar | Akilarの糖果屋</a></p><p>效果图如下:</p><p><img src="https://images.961110.xyz:5001/i/2022/04/28/Gitcalendar6f53266a44281ccf.png" alt="image-20220428222240724"></p><h4 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h4><ol><li><p>在博客根目录下执行下面指令。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install hexo-filter-gitcalendar --save</span><br></pre></td></tr></table></figure></li><li><p>添加配置信息。在站点配置文件<code>_config.yml</code> 或者主题配置文件如<code>_config.butterfly.yml</code> 中添加</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="comment"># hexo-filter-gitcalendar</span></span><br><span class="line"><span class="comment"># see https://akilar.top/posts/1f9c68c9/</span></span><br><span class="line"><span class="attr">gitcalendar:</span></span><br><span class="line"> <span class="attr">enable:</span> <span class="literal">true</span> <span class="comment"># 开关</span></span><br><span class="line"> <span class="attr">priority:</span> <span class="number">5</span> <span class="comment">#过滤器优先权</span></span><br><span class="line"> <span class="attr">enable_page:</span> <span class="string">/</span> <span class="comment"># 应用页面</span></span><br><span class="line"> <span class="comment"># butterfly挂载容器</span></span><br><span class="line"> <span class="attr">layout:</span> <span class="comment"># 挂载容器类型</span></span><br><span class="line"> <span class="attr">type:</span> <span class="string">id</span></span><br><span class="line"> <span class="attr">name:</span> <span class="string">recent-posts</span></span><br><span class="line"> <span class="attr">index:</span> <span class="number">0</span></span><br><span class="line"> <span class="attr">user:</span> <span class="string">jasonchio-cn</span> <span class="comment">#git用户名</span></span><br><span class="line"> <span class="attr">apiurl:</span> <span class="string">https://github-calendar-red.vercel.app</span></span><br><span class="line"> <span class="attr">minheight:</span></span><br><span class="line"> <span class="attr">pc:</span> <span class="string">280px</span> <span class="comment">#桌面端最小高度</span></span><br><span class="line"> <span class="attr">mibile:</span> <span class="string">0px</span> <span class="comment">#移动端最小高度</span></span><br><span class="line"> <span class="attr">color:</span> <span class="string">"['#e4dfd7', '#f9f4dc', '#f7e8aa', '#f7e8aa', '#f8df72', '#fcd217', '#fcc515', '#f28e16', '#fb8b05', '#d85916', '#f43e06']"</span> <span class="comment">#橘黄色调</span></span><br><span class="line"> <span class="comment"># color: "['#ebedf0', '#fdcdec', '#fc9bd9', '#fa6ac5', '#f838b2', '#f5089f', '#c4067e', '#92055e', '#540336', '#48022f', '#30021f']" #浅紫色调</span></span><br><span class="line"> <span class="comment"># color: "['#ebedf0', '#f0fff4', '#dcffe4', '#bef5cb', '#85e89d', '#34d058', '#28a745', '#22863a', '#176f2c', '#165c26', '#144620']" #翠绿色调</span></span><br><span class="line"> <span class="comment"># color: "['#ebedf0', '#f1f8ff', '#dbedff', '#c8e1ff', '#79b8ff', '#2188ff', '#0366d6', '#005cc5', '#044289', '#032f62', '#05264c']" #天青色调</span></span><br><span class="line"> <span class="attr">container:</span> <span class="string">.recent-post-item(style='width:100%;height:auto;padding:10px;')</span> <span class="comment">#父元素容器,需要使用pug语法</span></span><br><span class="line"> <span class="attr">gitcalendar_css:</span> <span class="string">https://npm.elemecdn.com/hexo-filter-gitcalendar/lib/gitcalendar.css</span></span><br><span class="line"> <span class="attr">gitcalendar_js:</span> <span class="string">https://npm.elemecdn.com/hexo-filter-gitcalendar/lib/gitcalendar.js</span></span><br></pre></td></tr></table></figure></li><li><p>其中 API 推荐自建,教程:<a href="https://akilar.top/posts/1f9c68c9/#%E8%87%AA%E5%BB%BAAPI%E9%83%A8%E7%BD%B2">Gitcalendar | Akilarの糖果屋</a></p></li></ol><h3 id="Sidebar-Card-Clock"><a href="#Sidebar-Card-Clock" class="headerlink" title="Sidebar Card Clock"></a>Sidebar Card Clock</h3><p>参考链接:<a href="https://akilar.top/posts/4e39cf4a/">Sidebar Card Clock | Akilarの糖果屋</a></p><p>效果如下:</p><p><img src="https://images.961110.xyz:5001/i/2022/04/28/Sidebar-Card-Clock.png" alt="image-20220428235053657"></p><h4 id="安装-1"><a href="#安装-1" class="headerlink" title="安装"></a>安装</h4><ol><li><p>在博客根目录下执行下面指令。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install hexo-butterfly-clock --save</span><br></pre></td></tr></table></figure></li><li><p>添加配置信息。在站点配置文件<code>_config.yml</code> 或者主题配置文件如<code>_config.butterfly.yml</code> 中添加</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="comment"># electric_clock</span></span><br><span class="line"><span class="comment"># see https://akilar.top/posts/4e39cf4a/</span></span><br><span class="line"><span class="attr">electric_clock:</span></span><br><span class="line"> <span class="attr">enable:</span> <span class="literal">true</span> <span class="comment"># 开关</span></span><br><span class="line"> <span class="attr">priority:</span> <span class="number">5</span> <span class="comment">#过滤器优先权</span></span><br><span class="line"> <span class="attr">enable_page:</span> <span class="string">all</span> <span class="comment"># 应用页面</span></span><br><span class="line"> <span class="attr">exclude:</span></span><br><span class="line"> <span class="comment"># - /posts/</span></span><br><span class="line"> <span class="comment"># - /about/</span></span><br><span class="line"> <span class="attr">layout:</span> <span class="comment"># 挂载容器类型</span></span><br><span class="line"> <span class="attr">type:</span> <span class="string">class</span></span><br><span class="line"> <span class="attr">name:</span> <span class="string">sticky_layout</span></span><br><span class="line"> <span class="attr">index:</span> <span class="number">0</span></span><br><span class="line"> <span class="attr">loading:</span> <span class="string">https://npm.elemecdn.com/hexo-butterfly-clock/lib/loading.gif</span> <span class="comment">#加载动画自定义</span></span><br><span class="line"> <span class="attr">clock_css:</span> <span class="string">https://npm.elemecdn.com/hexo-butterfly-clock/lib/clock.min.css</span></span><br><span class="line"> <span class="attr">clock_js:</span> <span class="string">https://npm.elemecdn.com/hexo-butterfly-clock/lib/clock.min.js</span></span><br><span class="line"> <span class="attr">ip_api:</span> <span class="string">https://pv.sohu.com/cityjson?ie=utf-8</span></span><br></pre></td></tr></table></figure></li><li><p>仅限于国内 IP,挂着代理加载不出来。</p></li></ol><h3 id="Swiper-Bar"><a href="#Swiper-Bar" class="headerlink" title="Swiper Bar"></a>Swiper Bar</h3><p>参考链接:<a href="https://akilar.top/posts/8e1264d1/">♪(^∇^*)欢迎回来!Swiper Bar | Akilarの糖果屋</a></p><p>效果图如下:</p><p><img src="https://images.961110.xyz:5001/i/2022/04/29/Swiper-Bar.png" alt="image-20220429172341824"></p><h4 id="安装-2"><a href="#安装-2" class="headerlink" title="安装"></a>安装</h4><ol><li><p>在博客根目录下执行下面指令。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install hexo-butterfly-swiper --save</span><br></pre></td></tr></table></figure></li><li><p>添加配置信息。在站点配置文件<code>_config.yml</code> 或者主题配置文件如<code>_config.butterfly.yml</code> 中添加</p><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="comment"># hexo-butterfly-swiper</span></span><br><span class="line"><span class="comment"># see https://akilar.top/posts/8e1264d1/</span></span><br><span class="line"><span class="attr">swiper:</span></span><br><span class="line"> <span class="attr">enable:</span> <span class="literal">true</span> <span class="comment"># 开关</span></span><br><span class="line"> <span class="attr">priority:</span> <span class="number">5</span> <span class="comment">#过滤器优先权</span></span><br><span class="line"> <span class="attr">enable_page:</span> <span class="string">all</span> <span class="comment"># 应用页面 (根目录就填’/‘, 分类页面就填’/categories/‘。若要应用于所有页面,就填’all’,默认为 all)</span></span><br><span class="line"> <span class="attr">timemode:</span> <span class="string">date</span> <span class="comment">#date/updated</span></span><br><span class="line"> <span class="attr">layout:</span> <span class="comment"># 挂载容器类型</span></span><br><span class="line"> <span class="attr">type:</span> <span class="string">id</span></span><br><span class="line"> <span class="attr">name:</span> <span class="string">recent-posts</span></span><br><span class="line"> <span class="attr">index:</span> <span class="number">0</span></span><br><span class="line"> <span class="attr">default_descr:</span> <span class="string">再怎么看我也不知道怎么描述它的啦!</span></span><br><span class="line"> <span class="attr">swiper_css:</span> <span class="string">https://npm.elemecdn.com/hexo-butterfly-swiper/lib/swiper.min.css</span> <span class="comment">#swiper css依赖</span></span><br><span class="line"> <span class="attr">swiper_js:</span> <span class="string">https://npm.elemecdn.com/hexo-butterfly-swiper/lib/swiper.min.js</span> <span class="comment">#swiper js依赖</span></span><br><span class="line"> <span class="attr">custom_css:</span> <span class="string">https://npm.elemecdn.com/hexo-butterfly-swiper/lib/swiperstyle.css</span> <span class="comment"># 适配主题样式补丁</span></span><br><span class="line"> <span class="attr">custom_js:</span> <span class="string">https://npm.elemecdn.com/hexo-butterfly-swiper/lib/swiper_init.js</span> <span class="comment"># swiper初始化方法</span></span><br></pre></td></tr></table></figure></li></ol><h3 id="友链朋友圈"><a href="#友链朋友圈" class="headerlink" title="友链朋友圈"></a>友链朋友圈</h3><p>参考链接:<a href="https://fcircle-doc.js.cool/#/">友链朋友圈</a></p><p>效果图如下(原博文示意图):</p><p><img src="https://images.961110.xyz:5001/i/2022/04/28/Swiper-Bar.png"></p><h4 id="安装-3"><a href="#安装-3" class="headerlink" title="安装"></a>安装</h4><ol><li><p>api 搭建方法参考:<a href="https://fcircle-doc.js.cool/#/backenddeploy">后端部署及基本配置</a></p></li><li><p>后端部署完之后,需要在前端部署,参考:<a href="https://fcircle-doc.js.cool/#/frontenddeploy">前端部署 (js.cool)</a></p></li><li><p>注意事项:如果能保证后端部署没有问题,而前端展示不出来的时候,F12看一下请求能否正确获取数据</p><p><img src="https://images.961110.xyz:5001/i/2022/05/21/firends-circle.png" alt="image-20220521130642099"></p><p>如果把<code>end</code>改成 10,就会返回正确的数据。bug 已经提了 issue,等待作者回复。</p></li></ol>]]></content>
<categories>
<category> 教程 </category>
</categories>
<tags>
<tag> 笔记 </tag>
</tags>
</entry>
<entry>
<title>Ubuntu 18.04 安装新版 nodejs</title>
<link href="/posts/42019.html"/>
<url>/posts/42019.html</url>
<content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>Ubuntu 18.04 上 apt 能安装的 nodejs 版本是 10.19.0,而官方早就更新到了 18.x,所以会有一些新的 module 不兼容。</p><table> <tr> <td> <center><img src="https://images.961110.xyz:5001/i/2022/04/25/apt_nodejs_version.png"> </center> </td> <td> <center><img src="https://images.961110.xyz:5001/i/2022/04/25/nodejs_offical_version.png" style="zoom:75%;"> </center> </td> </tr></table><h2 id="安装方法"><a href="#安装方法" class="headerlink" title="安装方法"></a>安装方法</h2><h3 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h3><p>根据官方 docs 的方法,需要增加 nodejs 的 repo 源</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -</span><br><span class="line">sudo apt install nodejs</span><br></pre></td></tr></table></figure><img src="https://images.961110.xyz:5001/i/2022/04/25/install_nodejs_repo.png" style="zoom:50%;" /><p>安装完成后查看版本号</p><p><img src="https://images.961110.xyz:5001/i/2022/04/25/current_nodejs_versione24699e3fcebb9b6.png" alt="image-20220425215020563"></p><h3 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h3><ol><li><p>建立 npm 包安装及缓存文件夹(如果建在/usr/local 目录下,后面运行安装的包会有权限问题)</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">mkdir -p ~/.npm/node_global</span><br><span class="line">mkdir -p ~/.npm/node_cache</span><br></pre></td></tr></table></figure></li><li><p>配置 npm</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">npm config set prefix "/home/jason/.npm/node_global"</span><br><span class="line">npm config set cache "/home/jason/.npm/node_cache"</span><br></pre></td></tr></table></figure></li><li><p>因为 npm 在国内比较慢,所以可以安装 cnpm,之后安装其他包可以使用 <code>cnpm install -g</code></p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">npm install cnpm -g --registry=https://registry.npm.taobao.org</span><br></pre></td></tr></table></figure><p>安装完成后建立 cnpm 的软链接</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">sudo ln -s /home/jason/.npm/node_global/lib/node_modules/cnpm/bin/* /usr/bin/</span><br></pre></td></tr></table></figure></li><li><p>配置环境变量</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">vim ~/.bashrc</span><br><span class="line"><span class="meta"></span></span><br><span class="line"><span class="meta">#</span><span class="bash"> 增加 nodejs 的环境变量</span></span><br><span class="line">export NODE_PATH=/home/jason/.npm/node_global/bin</span><br><span class="line">export PATH=$PATH:$NODE_PATH</span><br><span class="line"></span><br><span class="line">source ~/.bashrc</span><br></pre></td></tr></table></figure></li></ol><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><ol><li><a href="https://github.com/nodesource/distributions/blob/master/README.md">distributions/README.md at master · nodesource/distributions (github.com)</a></li><li><a href="https://blog.csdn.net/ggq89/article/details/107469808">Linux下Node.js的安装和配置_AlbertGou的博客-CSDN博客</a></li></ol>]]></content>
<categories>
<category> 教程 </category>
</categories>
<tags>
<tag> 教程 </tag>
</tags>
</entry>
<entry>
<title>群晖 Raid1 转 Basic</title>
<link href="/posts/53233.html"/>
<url>/posts/53233.html</url>
<content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>博主没有公网 IP,所以两个常用 PT 站的分享率都不算很高。提高分享率只能靠赚魔力值,需要挂一些大文件的种子,因此对硬盘空间要求较大。群晖里两块 4T 的硬盘以 Raid1 的方式保障数据安全,也因此损失了接近 4T 的空间,于是就有了将其转为 Basic 的想法。</p><h2 id="实操"><a href="#实操" class="headerlink" title="实操"></a>实操</h2><h3 id="友情提示"><a href="#友情提示" class="headerlink" title="友情提示"></a>友情提示</h3><p>理论上无损,但是没有人能保证 100% 成功,<strong>备份好重要文件再进行下面的操作</strong>。</p><h3 id="操作方法"><a href="#操作方法" class="headerlink" title="操作方法"></a>操作方法</h3><ol><li><p>群晖中插了两块硬盘,关机并拔出硬盘 2</p></li><li><p>开机,存储池会显示为<strong>堪用</strong>,不用慌,接着往下看。如果有报警可以在【控制面板】【硬件和电源】中【停止哔声】。</p><p><img src="https://images.961110.xyz:5001/i/2022/04/25/stop_hdd.png" alt="image-20220425193501824"></p></li><li><p>SSH 连接到群晖,输入如下命令查看硬盘的分区信息</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">cat /proc/mdstat</span><br></pre></td></tr></table></figure><p>由于在操作时没有做记录,此处引用的是参考博文中的图。</p><p><img src="https://images.961110.xyz:5001/i/2022/04/25/partition_info.jpg"></p><p>第1个分区 md0 为群晖系统分区,第2个分区 md1 为群晖交换分区,第3个分区 md2 为群晖存储分区(不知道为什么,博主的第三个分区是 md3)</p></li><li><p>输入如下命令,将存储分区转为 Basic。可能需要的时间较长,耐心等待.</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">mdadm --grow --raid-devices=1 --force /dev/md2</span><br></pre></td></tr></table></figure></li><li><p>刷新网页,存储池已经变成了良好,类型是 Basic,原来的文件也没有丢失。</p><p><img src="https://images.961110.xyz:5001/i/2022/04/25/Basic_4T.png" alt="image-20220425194822028"></p></li><li><p>将拔出的硬盘再插上,新建存储池就可以正常使用了。</p></li></ol><h3 id="数据安全保障"><a href="#数据安全保障" class="headerlink" title="数据安全保障"></a>数据安全保障</h3><p>博主分别用 Synology Drive 和 Moment 备份电脑和手机的重要资料,所有的资料都会在群晖的 home 目录下。Raid1 转为 Basic 之后,用 Cloud Sync 将 home 目录下的备份文件实时同步到了 OneDrive 中,相当于是又上了一道保险。</p><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><ol><li><a href="https://wp.gxnas.com/11613.html">简单两条命令让群晖存储空间类型为RAID1或者SHR无损转换成Basic - GXNAS博客</a></li></ol>]]></content>
<categories>
<category> 教程 </category>
</categories>
<tags>
<tag> 教程 </tag>
<tag> 群晖 </tag>
</tags>
</entry>
<entry>
<title>修改、合并、丢弃已push的commit message</title>
<link href="/posts/26938.html"/>
<url>/posts/26938.html</url>
<content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>对于某些重复性的提交(例如更新Readme),为了保持 commit message 的整洁,博主想把这些相同的内容合并成一份。</p><h2 id="commit-未-push"><a href="#commit-未-push" class="headerlink" title="commit 未 push"></a>commit 未 push</h2><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">git reset --soft HEAD~1</span><br></pre></td></tr></table></figure><p>HEAD^ 的意思是上一个版本,也可以写成HEAD<del>1。如果进行了2次commit,想都修改,可以使用HEAD</del>2(也可以直接使用 commitID)</p><ul><li><code>--mixed</code> 意思是:不删除工作空间改动代码,撤销 commit,并且撤销 git add 操作。</li><li><code>--soft</code> 意思是:不删除工作空间改动代码,撤销commit,不撤销 git add 操作。</li><li><code>--hard</code> 意思是:<strong>删除工作空间改动代码</strong>,撤销commit,并且撤销 git add 操作。</li></ul><h2 id="commit-已-push"><a href="#commit-已-push" class="headerlink" title="commit 已 push"></a>commit 已 push</h2><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">git rebase -i commitID</span><br></pre></td></tr></table></figure><p><code>git log</code>有如下 commit(按照提交时间顺序排列)</p><img src="https://images.961110.xyz:5001/i/2022/05/04/git_log.png" style="zoom:50%;" /><p>假设想要修改中间那条 commit(b20527091aa9cf6ea7116dd992a376a4b0ac647b),可以输入</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">git rebase -i 22f9aad873d155af6e354c8d92492a12015fcc11</span><br></pre></td></tr></table></figure><p><code>22f9aad873d155af6e354c8d92492a12015fcc11</code>是想要修改的 commit 的前一个 commitID,执行该条命令会出现如下内容(按照提交时间倒序排列)。</p><img src="https://images.961110.xyz:5001/i/2022/05/04/git_rebase55992eadca1d1afb.png" style="zoom:50%;" /><p>各个命令的含义如下:</p><ol><li><p><code>p, pick</code></p><p>保留 commit(默认)</p></li><li><p><code>r, reword</code></p><p>使用 commit,但修改 commit message。<code>:x</code>保存并退出后,执行 <code>git rebase continue</code>会再次打开一个文件,以对 commit message 进行修改。修改完保存之后继续进行 rebase。</p></li><li><p><code>e, edit</code></p><p>使用 commit,但修改提交</p></li><li><p><code>s, squash</code></p><p>使用该 commit,但和前一个 commit 合并,如果要合并多个 commit,需要将对应 commit 前的命令都改成 <code>squash</code>,合并的 commit 对应的 message 也会合并到一起。<code>:x</code>保存并退出后,执行 <code>git rebase continue</code>会再次打开一个文件,可以对合并后的 commit message 进行修改。</p></li><li><p><code>f, fixup</code></p><p>类似于 “squash”,但丢弃 commit message,即不合并 commit message。</p></li><li><p><code>x, exec</code></p><p>执行 shell 命令,这个没用过。</p></li><li><p><code>d, drop</code></p><p>丢弃该 commit,或者用删除这一行代替 <code>drop</code>。</p></li></ol><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><ol><li><a href="https://blog.csdn.net/realDonaldTrump/article/details/85050779">git丢弃、删除已经提交的commit</a></li><li><a href="https://www.jianshu.com/p/3f8664bb0a19">如何撤销未push的commit</a></li><li><a href="https://swumao.github.io/swumao/update/git/rebase/pick/edit/reword/drop/squash/fixup/2016/08/31/Git-rebase-%E5%90%88%E5%B9%B6%E5%A4%9A%E4%B8%AAcommit.html">git rebase 用来合并多个commit 使用介绍</a></li></ol>]]></content>
<categories>
<category> 教程 </category>
</categories>
<tags>
<tag> 教程 </tag>
<tag> 笔记 </tag>
<tag> git </tag>
</tags>
</entry>
<entry>
<title>OSS-Fuzz介绍</title>
<link href="/posts/51425.html"/>
<url>/posts/51425.html</url>
<content type="html"><![CDATA[<h2 id="软件漏洞挖掘技术"><a href="#软件漏洞挖掘技术" class="headerlink" title="软件漏洞挖掘技术"></a>软件漏洞挖掘技术</h2><p>进行软件漏洞挖掘时,通常有静态分析(static analysis)、动态分析(dynamic analysis)、符号执行(symbolic execution)、模糊测试(fuzzing)这几种技术手段。</p><h3 id="静态分析"><a href="#静态分析" class="headerlink" title="静态分析"></a>静态分析</h3><p>静态分析就是不真正的运行目标程序,但是通过对其进行各种语法、语义、数据流等的分析,来进行漏洞发掘。静态分析是由静态分析软件完成的,速度快,但是误报率高。</p><h3 id="动态分析"><a href="#动态分析" class="headerlink" title="动态分析"></a>动态分析</h3><p>动态分析就是一步步跟踪程序运行进行的分析。准确率很高,但是需要调试人员丰富的知识储备,而且很难进行大规模的程序漏洞挖掘。</p><h3 id="符号执行"><a href="#符号执行" class="headerlink" title="符号执行"></a>符号执行</h3><p>符号执行简单来说,就是试图找到什么输入对应什么样的运行状态,去覆盖所有的执行路径。因此,当被分析的程序比较复杂,有很多执行路径时,就会遇到路径爆炸的问题。</p><h3 id="模糊测试"><a href="#模糊测试" class="headerlink" title="模糊测试"></a>模糊测试</h3><p>模糊测试不需要人过多的参与,也不像动态分析那样要求分析人员有丰富的知识。简单来讲就是用大量的输入数据自动去执行程序,从而发现哪些输入能够使程序发生异常,进而分析可能存在的漏洞。当前比较成功的 fuzzer (执行模糊测试的程序)有 AFL、libFuzzer、OSS-Fuzz 等。</p><h2 id="OSS-Fuzz-介绍"><a href="#OSS-Fuzz-介绍" class="headerlink" title="OSS-Fuzz 介绍"></a>OSS-Fuzz 介绍</h2><p>OSS-Fuzz 能够针对开源软件进行持续的模糊测试,利用现代模糊测试技术与可拓展的分布式执行相结合,提高通用开源软件的安全性与稳定性。OSS-Fuzz 结合了多种模糊测试技术/漏洞捕捉技术(libfuzzer)与清洗技术(AddressSanitizer),并且通过 ClusterFuzz 为大规模可分布式执行提供了测试环境。闭源项目可以运行私有的 ClusterFuzz 或 ClusterFuzzLite 实例进行模糊测试。</p><p>目前,OSS Fuzz支持 C/C++、Rust、Go、Python和 Java/JVM 以及其他 LLVM 支持的语言,支持对 x86_64 和 i386 平台的程序进行模糊化。</p><img src="https://github.com/google/oss-fuzz/raw/master/docs/images/process.png" style="zoom:50%;" /><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><ol><li><a href="https://github.com/google/oss-fuzz">OSS-Fuzz - continuous fuzzing for open source software</a></li><li><a href="https://www.jianshu.com/p/6fcdf0f9b370">从AFL开始FUZZ之旅</a></li></ol>]]></content>
<categories>
<category> 基础知识 </category>
</categories>
<tags>
<tag> 笔记 </tag>
</tags>
</entry>
<entry>
<title>【转载】Markdown嵌入B站视频</title>
<link href="/posts/64427.html"/>
<url>/posts/64427.html</url>
<content type="html"><![CDATA[<h2 id="嵌入代码"><a href="#嵌入代码" class="headerlink" title="嵌入代码"></a>嵌入代码</h2><p>首先在B站视频详情页找到分享界面,点击下图中“嵌入代码”,会将iframe复制到剪切板</p><p><img src="https://images.961110.xyz:5001/i/2022/03/29/B.png"></p><p>在 Markdown 文档中插入如下代码,将代码中<code>video_url</code>替换为对应视频的<code>src</code>,结尾加一个<code>&high_quality=1</code>可以将默认画质设置为最高</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line"><div style="position: relative; width: 100%; height: 0; padding-bottom: 75%;"></span><br><span class="line"><iframe src="video_url&page=1&high_quality=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" style="position: absolute; width: 100%; height: 100%; left: 0; top: 0;"> </iframe></span><br><span class="line"></div></span><br></pre></td></tr></table></figure><p>插入的效果如下图</p><div style="position: relative; width: 100%; height: 0; padding-bottom: 75%;"><iframe src="//player.bilibili.com/player.html?aid=97136120&bvid=BV1F7411o7Q6&cid=165826337&page=1&page=1&high_quality=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" style="position: absolute; width: 100%; height: 100%; left: 0; top: 0;"> </iframe></div><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><ol><li><a href="https://www.tamsiree.com/TechnicalResearch/Hexo/Hexo%E6%8F%92%E5%85%A5Bilibili%E8%A7%86%E9%A2%91%E8%87%AA%E9%80%82%E5%BA%94/">Hexo插入Bilibili视频自适应 | Tamsiree</a></li></ol>]]></content>
<categories>
<category> 教程 </category>
</categories>
<tags>
<tag> 教程 </tag>
<tag> 笔记 </tag>
</tags>
</entry>
<entry>
<title>【转载】装机注意事项</title>
<link href="/posts/16750.html"/>
<url>/posts/16750.html</url>
<content type="html"><![CDATA[<blockquote><p>【转载】以下内容均来源于B站UP主乌尔巴工作室,详细内容请直接观看视频。</p></blockquote><div style="position: relative; width: 100%; height: 0; padding-bottom: 75%;"><iframe src="//player.bilibili.com/player.html?aid=296985388&bvid=BV1bF411b7Uc&cid=700524206&page=1&high_quality=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" style="position: absolute; width: 100%; height: 100%; left: 0; top: 0;"> </iframe></div><h2 id="CPU"><a href="#CPU" class="headerlink" title="CPU"></a>CPU</h2><ol><li>检查CPU和主板型号是否匹配及合理</li><li>检查CPU是否自带核显</li></ol><h2 id="散热器"><a href="#散热器" class="headerlink" title="散热器"></a>散热器</h2><ol><li>检查散热器是否对应CPU平台型号</li><li>检查散热器是否能够压住CPU发热量</li><li>检查散热器是否附带导热硅脂</li><li>检查散热器是否匹配机箱尺寸</li><li>检查主板是否有足够的风扇供电接口及灯光接口</li></ol><h2 id="主板"><a href="#主板" class="headerlink" title="主板"></a>主板</h2><ol><li>检查主板能否和机箱尺寸匹配</li></ol><h2 id="内存"><a href="#内存" class="headerlink" title="内存"></a>内存</h2><ol><li>检查主板支持的内存数量以及型号</li><li>检查CPU主板支持的内存频率</li></ol><h2 id="硬盘"><a href="#硬盘" class="headerlink" title="硬盘"></a>硬盘</h2><ol><li>检查主板是否有足够的硬盘插槽和接口</li><li>检查主板支持的硬盘协议</li><li>检查硬盘数据线数量</li><li>检查机箱硬盘位与硬盘数量是否匹配</li></ol><h2 id="显卡"><a href="#显卡" class="headerlink" title="显卡"></a>显卡</h2><ol><li>检查机箱支持的显卡尺寸</li><li>检查显示器接口与显卡接口是否相匹配</li></ol><h2 id="电源"><a href="#电源" class="headerlink" title="电源"></a>电源</h2><ol><li>检查机箱支持的电源尺寸</li><li>检查电源功率是否满足整机功率</li><li>检查电源供电接口是否满足硬件条件</li></ol><h2 id="机箱风扇"><a href="#机箱风扇" class="headerlink" title="机箱风扇"></a>机箱风扇</h2><ol><li>检查机箱风扇位能否满足风扇尺寸及数量</li><li>检查主板接口是否满足风扇供电及灯效控制</li></ol>]]></content>
<categories>
<category> 教程 </category>
</categories>
<tags>
<tag> 教程 </tag>
<tag> 笔记 </tag>
<tag> 装机 </tag>
<tag> 桌搭 </tag>
</tags>
</entry>
<entry>
<title>【转载】树莓派4B 的系统备份方法大全(全卡+压缩备份)</title>
<link href="/posts/28657.html"/>
<url>/posts/28657.html</url>
<content type="html"><![CDATA[<blockquote><p>【转载】原文地址 <a href="https://post.smzdm.com/p/apzkgne7/#cl_0">树莓派学习笔记 篇四:树莓派 4B 的系统备份方法大全(全卡 + 压缩备份)</a></p></blockquote><h1 id="树莓派-4B-的系统备份方法大全(全卡-压缩备份)"><a href="#树莓派-4B-的系统备份方法大全(全卡-压缩备份)" class="headerlink" title="树莓派 4B 的系统备份方法大全(全卡 + 压缩备份)"></a>树莓派 4B 的系统备份方法大全(全卡 + 压缩备份)</h1><p>2019-12-09 10:23:52 29 点赞 192 收藏 12 评论</p><p>在 Windows 上用 VMware 安装 Linux 虚拟机有个「快照」的功能,就是把你当前的系统做个备份,一旦后来误操作把系统搞挂了之后可以恢复到备份的那个时间节点。Linux 系统开放的权限比较高,以 root 权限操作的话很有可能就会误删一些系统文件导致系统崩溃。我就曾把树莓派的系统搞挂过好几次,每次只能重新刷入镜像,开机后还得重复一大堆操作,所以备份树莓派的系统就很重要了。还可以把已经部署好的树莓派系统,批量复制到更多的树莓派上。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_2/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bcbf8901506.jpg_e1080.jpg"></a></p><p>系统备份分为<strong>「全卡备份」</strong>和<strong>「压缩备份」</strong>两种。「全卡备份」就是将 SD 卡整体备份,操作简单,SD 卡是多大空间的备份的镜像就有多大,系统还原时需要更大的 SD 卡才行。「压缩备份」只备份 SD 卡中有内容的分区,备份出来的镜像大小基本和原来的镜像文件差不多,方法有点复杂,备份的镜像体积大大减小。原始的树莓派官方镜像第一次启动后会自动扩展整个 SD 卡空间,恢复备份镜像后首次开机需要 sudo raspi-config 扩展 SD 卡使用空间。</p><blockquote><p>本文需要用到 Linux 系统的挂载和分区的一些知识,相关内容可以查看该系列的第三篇。</p></blockquote><p><a href="https://post.smzdm.com/p/a25rzkxn"><img src="https://images.961110.xyz:5001/i/2022/01/25/5dda52fd02d8348.jpg_a200.jpg"></a><a href="https://post.smzdm.com/p/a25rzkxn">树莓派学习笔记 篇三:树莓派 4B 与移动存储设备的那些事儿</a>本文是「树莓派学习笔记」系列的第三篇,将学习下 Linux 系统分区的基础知识,在树莓派上如何挂载与卸载外接的移动存储设备,不同分区格式的硬盘速度测试以及如何在一张 TF 卡上实现多系统启动。1.SD 卡与 TF 卡内存卡是生活中很常见的一种存储设备,之前常常分不清楚 SD 卡和 TF 卡的区别,SD 卡是 SecureDi<a href="https://zhiyou.smzdm.com/member/1000390420/">BigBubbleGum</a>| _赞_40 _评论_14 _收藏_284 <a href="https://post.smzdm.com/p/a25rzkxn">查看详情</a></p><h2 id="一、全卡备份"><a href="#一、全卡备份" class="headerlink" title="一、全卡备份"></a>一、全卡备份</h2><h3 id="1-Win32DiskImager"><a href="#1-Win32DiskImager" class="headerlink" title="1. Win32DiskImager"></a>1. Win32DiskImager</h3><p>全卡备份是最简单的一种,首先在硬盘上创建一 img 后缀的空文件,打开 Win32DiskImager,选择刚刚创建的空 img 文件和 SD 卡盘符,点击 read 既可。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_3/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bcc031f2090.jpg_e1080.jpg"></a></p><p>等待几分钟后就得到了备份的 img 文件,用 Win32DiskImager 来制作镜像时因为无法读取到 Linux 分区,所以是全卡备份,该方法的缺点是备份文件会和 SD 卡的容量一致,而且在还原的时候必须使用比镜像更大容量的 SD 卡。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_4/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bcbea997501.jpg_e1080.jpg"></a></p><p>可以看到 Raspbian 原系统只有 3.5G,备份文件是 7.4G(8G 的 SD 卡)。</p><h3 id="2-树莓派上直接复制系统"><a href="#2-树莓派上直接复制系统" class="headerlink" title="2. 树莓派上直接复制系统"></a>2. 树莓派上直接复制系统</h3><p>准备一张空白 SD 卡,格式化 SD 卡插入<a href="https://www.smzdm.com/fenlei/dukaqi/">读卡器</a>,然后插入树莓派上,使用 umount 卸载该挂载,使用命令 dd bs=4M if=/dev/mmcblk0 of=/dev/sda 就可以把树莓派系统内容全部拷贝到 SD 卡上,mmcblk0 就是树莓派上系统 TF 卡,sda 就是插入的 SD 卡(使用 lsblk 查看),等待完成后既可以关机插入新卡重启。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_5/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bcbc42e4870.jpg_e1080.jpg"></a></p><p>除了将原系统拷贝到 SD 卡外,也可以拷贝到 U 盘之类的移动<a href="https://www.smzdm.com/fenlei/cunchushebei/">存储设备</a>,树莓派 3B+ 支持直接从 USB 启动系统,树莓派 4B 暂时还不支持。</p><p>通过 sudo blkid 获取已烧录好系统的 U 盘或者<a href="https://www.smzdm.com/fenlei/yidongyingpan/">移动硬盘</a>的 PARTUUID。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_6/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bcbcce07160.jpg_e1080.jpg"></a></p><p>将系统 TF 卡中 / boot/cmdline.txt 中 rootpartuuid 修改为上述 uuid 重启,即可从 USB 设备启动系统。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_7/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bd4671f891.jpg_e1080.jpg"></a></p><p>该方法本质上是将原系统镜像写入另外一张 SD 卡,适合将你的系统复制给小伙伴用,不适合用作备份。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_8/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bd7548a5458.jpg_e1080.jpg"></a></p><p>树莓派的桌面系统自带 SD Card copier,无需输入命令,也可完成复制过程。</p><h3 id="3-dd-命令"><a href="#3-dd-命令" class="headerlink" title="3. dd 命令"></a>3. dd 命令</h3><p>在树莓派上 mount 一个共享目录(空间大于系统的 TF 卡),然后直接用 dd 就可以备份树莓派 dd if=/dev/mmcblk0 of=raspberrypi.img bs=1M 共享目录在电脑上的话建议用网线连接,无线网会很慢。</p><p>或者在 Ubuntu 虚拟机中操作(会自动挂载 SD 卡,未自动挂载 SD 卡则需要先手动挂载)。下图中的 /dev/sdb1 /dev/sdb2 就是树莓派的 SD 卡,其中 256M 的分区 sdb1 是 boot 分区,7.1G 的分区 sdb2 是树莓派的系统文件分区。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_9/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bd7e83f7141.jpg_e1080.jpg"></a></p><p>使用 dd 命令可以直接备份 SD 卡。这里树莓派的 SD 卡的路径是 /dev/sdb1 和 /dev/sdb2 ,所以备份整个 SD 卡的路径就是 /dev/sdb。</p><p>输入备份命令:sudo dd if=/dev/sdb | gzip>/home/hanson/Documents/rpi-back2.gz,其中备份文件要保存的位置、文件名和 SD 卡的路径要根据实际选择。备份的时候终端没有进度条,像卡死一样,等着就行。可以另开一个终端,运行如下命令,可以观察到目标文件大小的变化。</p><p>watch -d -n 5 ls -lh /home/hanson/Documents/rpi-back2.gz</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_10/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bd8cce9245.jpg_e1080.jpg"></a></p><p>这个命令可以在备份的同时打包文件,压缩完成后的 .gz 文件只有 1.9G,解压后是 7.5G,本质上也是全卡复制。</p><p>还原的时候在 Windows 下解压文件需要加上. img 后缀。Linux 下的还原方法 sudo gzip -dc /home/hanson/Documents/rpi-back2.gz | sudo dd of=/dev/sda</p><h2 id="二、压缩备份"><a href="#二、压缩备份" class="headerlink" title="二、压缩备份"></a>二、压缩备份</h2><h3 id="1-原理"><a href="#1-原理" class="headerlink" title="1. 原理"></a>1. 原理</h3><p>树莓派的官方系统是基于 Debian 的,主要是两个分区:启动分区(boot)、根分区(root),烧录系统的时候就是将这两个分区写入 SD 卡。首次开机后需使用 raspi-config 来扩展 root 分区的大小,boot 分区不变,来达到使用所有 SD 卡内容量的目的(最新的系统已经不需要扩展操作)。</p><blockquote><p>boot 分区:fat32 格式,挂载点 / boot,存放一些系统启动需要的基本文件,包括内核、驱动、firmware、启动脚本等,可以在 Windows 上打开读取。</p><p>root 分区:ext4 格式,挂载点 /,存放一些安装的软件和库文件、系统配置、用户数据等。 另外当系统启动时会自动生成和挂载一些必要的其他<a href="https://www.smzdm.com/fenlei/wenjianjia/">文件夹</a>,包括 temfs、sysfs、proc、debugfs、configfs 等虚拟文件系统,由操作系统自动管理,备份时不需要关注。root 分区在 Windows 上无法识别,所以每次将 SD 卡插到 Windows 电脑上会弹出来格式化的选项,千万不要点。</p></blockquote><p><strong>对于树莓派系统的备份,主要就是对 boot 和 root 分区的备份。</strong>而 root 分区里面只有一部分空间存储有内容,剩下的部分是空的,如果直接备份的话那就是整个 SD 卡空间备份,也就是上面所说的全卡备份。而压缩备份则是备份 boot 分区和 root 分区中有内容的空间。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_11/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bdb43917481.jpg_e1080.jpg"></a></p><h3 id="2-PiShrink-裁剪"><a href="#2-PiShrink-裁剪" class="headerlink" title="2. PiShrink 裁剪"></a>2. PiShrink 裁剪</h3><p>PiShrink 是 Github 上开源的树莓派压缩工具,通过裁剪上面用 Win32DiskImager 或者 dd 命令全卡备份的镜像,去掉没有内容的分区,从而减小备份镜像的大小。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_12/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bdc2b9d2092.jpg_e1080.jpg"></a></p><p>先将全卡备份的镜像文件复制到 Linux 中,打开终端执行 wget <a href="https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh">https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh</a> 下载 sh 脚本文件,默认存到 / home/user 目录下,将其拷贝到镜像所在文件夹下。执行 chmod +x pishrink.sh 增加执行权限,然后执行 sudo bash pishrink.sh rpi-back.img 即可。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_13/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bdd80dd1093.jpg_e1080.jpg"></a></p><p>原来 7.5GB 的备份文件就被缩减到 3.4GB。</p><p>如果你的 linux 系统是语言是中文,可能会报错,需要制定英文运行 sudo pishrink.sh raspberrypi.img</p><p>sh 脚本文件裁剪的方法比较简单,比如果你想要在 Linux 单纯使用命令行方式裁剪全卡备份文件,可以参考<a href="https://blog.csdn.net/talkxin/article/details/50464313">使用 linux 裁剪树莓派完整 sd 卡镜像</a></p><h3 id="3-gparted"><a href="#3-gparted" class="headerlink" title="3. gparted"></a>3. gparted</h3><p>根据上述压缩备份原理,先在 Linux 环境中将 root 分区拆分内容空间和空白空间,然后在 Windows 上用 Win32DiskImager 软件仅读取内容空间进行备份。以下以 Windows 10 64 位 + 虚拟机 Ubuntu 19.10 为例,对树莓派 4B 的 Raspbian 系统进行压缩备份。</p><p>(1). 打开虚拟机,在终端输入 sudo apt install gparted 安装 gparted</p><p>(2). 将需要备份的 SD 卡插入读卡器然后插入电脑,等待 Linux 读取成功。</p><p>(3). 输入 sudo gparted 启动 gparted,在右上角选择 SD 卡,可以看到 ext4 分区当中置使用了 3.78G,剩余 10.88G 都是空白的。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_14/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02be260035082.jpg_e1080.jpg"></a></p><p>(4). 在 ext4 分区上右击点击 umount 卸载,然后继续右键点击更改大小。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_15/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02be552ef2427.jpg_e1080.jpg"></a></p><p>(5). 在弹出的窗口中重新分配大小,在新大小一栏填入比最小值(3689)大上三四百就行,我这里大 300M,在点击空余空间会自动计算,然后点击右下角 Resize,之后可以看到原来的分区只剩下 4.07G,剩下的 10.58G 空间未分配。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_16/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02be49db03575.jpg_e1080.jpg"></a></p><p>(6). 然后点击绿色的对号键确认,在弹出的对话框中点应用。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_17/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02be5e3b74763.jpg_e1080.jpg"></a></p><p>(7). 等待执行完成,可以看到分区由 14.66G 缩小到 4.07G,然后弹出 SD 卡,到这里 Linux 上的操作完成。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_18/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02be6d4ec1683.jpg_e1080.jpg"></a></p><p>(8). 将 SD 卡重新插到 Windows 上,新建一个空白 img 文件,打开 Win32DiskImager 软件,选中该文件,然后勾选‘’仅读取已分配分区‘’(注意要最新版,旧版本没有这个功能)。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_19/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02becea5d6117.jpg_e1080.jpg"></a></p><p>(9). 等待读取完成,压缩的镜像备份就制作完成了,源镜像文件 3.9GB,备份的镜像文件 4.5GB,如果压缩一下的话就只有 1.8GB 了,适合存储。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_20/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bee0b1b245.jpg_e1080.jpg"></a></p><h3 id="4-树莓派-Linux-命令行"><a href="#4-树莓派-Linux-命令行" class="headerlink" title="4. 树莓派 / Linux 命令行"></a>4. 树莓派 / Linux 命令行</h3><p>这种方法完全利用 Linux 命令操作,使用 dump 和 restore 来制作树莓派的 img 镜像,具体过程来自 <a href="https://blog.csdn.net/zhufu86/article/details/78821056">CSDN 的 Blog</a>,我按照文中步骤实际操作了一番。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_21/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bf1f4551865.jpg_e1080.jpg"></a></p><p>(1). 查看系统所占容量</p><p>先用 df -h 命令查看当前系统所占容量,以确定将来要生成备份镜像的大小,若树莓派所占空间小于 SD 卡空间的 50%,就可以在树莓派内部直接生成镜像。否则要将备份的镜像生成到挂载的外置 SD 卡里面。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_22/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bf23b9d2742.jpg_e1080.jpg"></a></p><p>可以看到树莓派有 boot 和 root 两个分区,所占大小为 3.5G+253M=3.8G,因为格式转换,所以会损耗 5%~10%,总大小大约 3.8*1.1=4.2G。如果 SD 卡剩余空间大于这么多就可以接着往下进行了。</p><p>(2) 安装所需的软件</p><p>sudo apt-get install dosfstools dump parted kpartx</p><p>dosfstools:fat32 分区格式化工具 dump:dump & restore 备份工具 parted & kpartx:虚拟磁盘工具</p><p>(3). 根据第一步计算的系统所占用空间,用 dd 命令创建一个大约 4.5G 的空白镜像 raspberrypi.img,这个过程大约几分钟完成。 sudo dd if=/dev/zero of=raspberrypi.img bs=1M count=4500</p><p>注意这里 bs=1M(1M=1024x1024Bytes)。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_23/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bf1db801044.jpg_e1080.jpg"></a></p><p>(4). 分割虚拟磁盘</p><p>通过 sudo fdisk -l /dev/mmcblk0 得知:第一个分区为 boot 分区,采用 FAT32 格式,由 sector 8192 开始到 sector 532480;第二个分区采用 EXT4,由 sector 540672 开始到空白 img 结尾。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_24/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bf5255c9057.jpg_e1080.jpg"></a></p><p>sudo parted raspberrypi.img –script – mklabel msdos</p><p>sudo parted raspberrypi.img –script – mkpart primary fat32 8192s 532480s</p><p>sudo parted raspberrypi.img –script – mkpart primary ext4 540672s -1</p><p>分区的起始扇区数都是 8192 的倍数,上述命令中 8192 基本是固定的,532480 和 540672 根据你自己的实际查询情况修改,-l 表示到文件末尾。</p><p>检查分区是否成功 sudo parted raspberrypi.img, 在 parted 程序中,输入 print free 命令可以显示分区内容,输入 quit 退出。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_25/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bf99bc19328.jpg_e1080.jpg"></a></p><p>(5). 挂载虚拟磁盘并格式化</p><p>设置 img 文件对应的 Loop device:sudo losetup -f –show raspberrypi.img 下一行命令输出了 /dev/loop0。(如果不是 loop0 的话以后的各个步骤里的 loop0 都要改成和这里一样的)。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_26/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bfa5c101523.jpg_e1080.jpg"></a></p><p>接着 sudo kpartx -va /dev/loop0</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_27/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bfa57284077.jpg_e1080.jpg"></a></p><p>此时 loop device 就设置好了,loop0p1 对应的是 img 文件分区上的 /boot,loop0p2 对应的是 /(根)。</p><p>然后格式化 img 文件中的两个分区。</p><p>sudo mkfs.vfat -n boot /dev/mapper/loop0p1</p><p>sudo mkfs.ext4 /dev/mapper/loop0p2</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_28/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bfbe2c16347.jpg_e1080.jpg"></a></p><p>挂载目标 img 文件 loop device 到新建的目录下。</p><p>mkdir tgt_boot tgt_Root sudo mount -t vfat -o uid=pi,gid=pi,umask=0000 /dev/mapper/loop0p1 ./tgt_boot/ sudo mount -t ext4 /dev/mapper/loop0p2 ./tgt_Root/</p><p>这里是在树莓派系统里面做备份操作的,如果是用其他 Linux 系统,请注意 uid 和 gid 的设置。</p><p>(6). 开始备份</p><p><strong>【备份 / boot】</strong></p><p>首先备份 /boot,直接拷贝即可 sudo cp -rfp /boot/* ./tgt_boot/(如果是用其他 Linux 系统,/boot/* 换成你挂载的 SD 卡,比如 dev/sdb1)</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_29/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bfd394b7938.jpg_e1080.jpg"></a></p><p>运行完出来一堆提示:failed to preserve ownership for…,进图形界面查看了下:</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_30/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02bff42073339.jpg_e1080.jpg"></a></p><p>文件都复制成功了,只是文件的权限没有复制过来,影响不大。</p><p><strong>【备份 “/”】</strong></p><p>备份根文件系统用 dump/restore 方法,首先对目标挂载点设置合适的权限,并清空。(如果是用其他 Linux 系统,将 pi.pi 替换成对应的用户名和用户组)</p><p>sudo chmod 777 tgt_Root sudo chown pi.pi tgt_Root sudo rm -rf ./tgt_Root/* cd tgt_Root/</p><p>然后开始备份</p><p>sudo dump -0uaf - / | sudo restore -rf - (如果是用其他 Linux 系统,/ 换成你挂载的 SD 卡,比如 dev/sdb2)</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_31/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02c04fd9a6007.jpg_e1080.jpg"></a></p><p>整个 dump/restore 过程需要几分钟时间,看到 DUMP IS DONE 就说明备份成功了。</p><p>如果上述方法的 sudo dump … 那一步出现 Broken pipe、 Illegal instruction 等错误而失败的话,可以使用 tar 方法,把源 SD 卡的根文件系统打包,所以在本机系统空间里需要额外的和源 SD 卡的根文件系统已用空间大小一样的可用空间,详细过程见原作者。</p><p>(7). 修改相应的 PARTUUID</p><p>此时备份已经完成,还需要修改下 Raspbian 启动对应分区的 PARTUUID,也就是修改目标 img 文件里的下面两个文件:./tgt_boot/cmdline.txt 和./tgt_Root/etc/fstab</p><p>首先查看 img 文件对应的 loop device 的两个分区的 PARTUUID: sudo blkid</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_32/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02c072cee9137.jpg_e1080.jpg"></a></p><p>这里可以看到 /boot 对应的是 594e62d6-01 , / 对应的是 594e62d6-02</p><p>修改 cmdline.txt 文件 sudo vi ./tgt_boot/cmdline.txt</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_33/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02c07dced5950.jpg_e1080.jpg"></a></p><p>修改 fstab 文件 sudo vim ./tgt_Root/etc/fstab</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_34/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02c096be89431.jpg_e1080.jpg"></a></p><p>(8). 收尾工作</p><p>卸载各个挂载的分区: sudo umount tgt_boot tgt_Root</p><p>删除 loop device:sudo kpartx -d /dev/loop0 和 sudo losetup -d /dev/loop0</p><p>删除挂载点目录:rmdir tgt_boot tgt_Root</p><p>最后只剩下 img 文件</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_35/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02c0de68c2683.jpg_e1080.jpg"></a></p><p>将其通过 WinSCP 拷贝到 Windows 电脑上保存。或者直接在树莓派中 dd if=/home/xxx/xxx.img of=/dev/sdb bs=4M ; sync 烧写 img 文件到其他 SD 卡中(镜像目录使用 if= 来指定,外接 U 盘设备的地址使用 of= 来指定,不一定是 sdb,最后的 bs=4M 指定一次写入字节数,sync 同步数据),同样的在树莓派中运行后,要先用 raspi-config 先把分区空间 expand 一下。</p><p>这种方法虽然步骤比较多,但是照着一步步走下来可以对整个备份的全部技术细节有详细的了解。</p><h3 id="5-sh-脚本一键备份"><a href="#5-sh-脚本一键备份" class="headerlink" title="5. sh 脚本一键备份"></a>5. sh 脚本一键备份</h3><p>上述纯 Linux 命令行进行压缩备份的步骤繁琐,可以把全部步骤写成一个 .sh 脚本文件,这样的话只需要执行该脚本文件就能自动执行完压缩备份命令了。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="meta">#!/bin/sh</span></span><br><span class="line">sudo apt-get install dosfstools dump parted kpartx</span><br><span class="line">sudo dd <span class="keyword">if</span>=/dev/zero of=raspberrypi.img bs=1M count=4500</span><br><span class="line"><span class="comment"># 通过 count 更改创建的空白镜像大小(按照自己树莓派 TF 卡已用空间更改)</span></span><br><span class="line">sudo parted raspberrypi.img --script -- mklabel msdos</span><br><span class="line">sudo parted raspberrypi.img --script -- mkpart primary fat32 8192s 532480s</span><br><span class="line">sudo parted raspberrypi.img --script -- mkpart primary ext4 540672s -1</span><br><span class="line"><span class="comment"># 给 img 文件分区</span></span><br><span class="line">loopdevice=`sudo losetup -f --show raspberrypi.img`</span><br><span class="line">device=`sudo kpartx -va <span class="variable">$loopdevice</span> | sed -E <span class="string">'s/.*(loop[0-9])p.*/1/g'</span> | head -1`</span><br><span class="line">device=<span class="string">"/dev/mapper/<span class="variable">${device}</span>"</span></span><br><span class="line">partBoot=<span class="string">"<span class="variable">${device}</span>p1"</span></span><br><span class="line">partRoot=<span class="string">"<span class="variable">${device}</span>p2"</span></span><br><span class="line">sudo mkfs.vfat -n boot <span class="variable">$partBoot</span></span><br><span class="line">sudo mkfs.ext4 <span class="variable">$partRoot</span></span><br><span class="line"><span class="comment"># 空白镜像挂载并格式化,注意修改数值</span></span><br><span class="line">mkdir tgt_boot tgt_Root</span><br><span class="line">sudo mount -t vfat -o uid=pi,gid=pi,<span class="built_in">umask</span>=0000 <span class="variable">$partBoot</span> ./tgt_boot/</span><br><span class="line">sudo mount -t ext4 <span class="variable">$partRoot</span> ./tgt_Root/</span><br><span class="line"><span class="comment"># 拷贝至镜像</span></span><br><span class="line">sudo cp -rfp /boot/* ./tgt_boot/</span><br><span class="line"><span class="comment"># 拷贝 / boot</span></span><br><span class="line">sudo chmod 777 tgt_Root</span><br><span class="line">sudo chown pi.pi tgt_Root</span><br><span class="line">sudo rm -rf ./tgt_Root/*</span><br><span class="line"><span class="built_in">cd</span> tgt_Root/</span><br><span class="line">sudo dump -0uaf - / | sudo restore -rf -</span><br><span class="line"><span class="comment"># 拷贝 / rootfs</span></span><br><span class="line">sudo umount tgt_boot tgt_Root</span><br><span class="line">sudo kpartx -d /dev/loop0</span><br><span class="line">sudo losetup -d /dev/loop0</span><br><span class="line">rmdir tgt_boot tgt_Root</span><br><span class="line"></span><br><span class="line"><span class="comment"># 收尾工作</span></span><br><span class="line">使用上述脚本只需要在树莓派中新建一个. sh 文件 sudo vi backup.sh,sudo chmod 777 backup.sh 更改权限,然后 sudo ./backup.sh 就可以在当前脚本目录中生成树莓派的备份镜像文件了,然后还需要手动修改下 PARTUUID。之后既可以在 Windows 中用 Win32DiskImager 将镜像恢复到 SD 卡,也可以在 Linux 用 dd 命令还原到 SD 卡,当用此 SD 卡启动树莓派的时候执行 raspi-config ->Expand Filesystem 即可扩展未使用的空间。</span><br></pre></td></tr></table></figure><p>sh 脚本首先需要能够读懂才能使用,里面有的参数需要修改以适合你自己的环境。sh 脚本如果报错了不容易查找问题原因,建议还是先把 Linux 命令的全部流程走通了再来用这个脚本,这样的可以知道哪一步出错了从而修改。</p><h3 id="6-推荐脚本"><a href="#6-推荐脚本" class="headerlink" title="6. 推荐脚本"></a>6. 推荐脚本</h3><p>这里还有一个<a href="https://blog.csdn.net/qingtian11112/article/details/99825257">脚本</a>是我最推荐使用的,程序内容太长,我就不贴上来了,具体脚本内容我也没有完全读懂,反正简单易用,也是我目前使用的备份方法,可以<a href="https://github.com/BigBubbleGum/RaspberryBackup">去这里下载</a>,使用方法如下:</p><p>step1:下载脚本文件 rpi-backup.sh 到 Linux 系统中</p><p>step2:把需要备份的 SD 卡插入 Linux 系统中,用 df -h 命令查询下 SD 卡对应的设备名。</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_36/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02c0ed7b03186.jpg_e1080.jpg"></a></p><p>step3:进入脚本文件 rpi-backup.sh 所在目录,只需要下面两行命令即可完成 SD 卡备份,最终 img 文件会生成在~/backupimg / 文件夹下。</p><p>sudo chmod +x rpi-backup.sh (需要赋可执行权限)</p><p>./rpi-backup.sh /dev/sdb1 /dev/sdb2(脚本执行有两个参数,第一个参数是树莓派 SD 卡 / boot 分区的设备名:/dev/sdb1,第二个参数是 / 分区的设备名:/dev/sdb2,视情况修改)</p><p><a href="https://post.smzdm.com/p/apzkgne7/pic_37/"><img src="https://images.961110.xyz:5001/i/2022/01/25/5ded02c112bae1721.jpg_e1080.jpg"></a></p><h2 id="三、总结"><a href="#三、总结" class="headerlink" title="三、总结"></a>三、总结</h2><hr><p>全卡备份肯定是不可取的,如果你是新手小白的话,建议先在 Windows 上用 Win32DiskImager 或者树莓派上的 SD Card copier 全卡备份,然后用 PiShrink 进行裁剪,代码量最低,简单不容易出错,就是比较耗时。</p><p>如果你熟悉一点 Linux 的基本操作的话可以在 Linux 虚拟机中用 gparted 图形界面来进行备份。</p><p>如果你是树莓派爱好者或者像我一样想深入学习下树莓派的系统备份技术原理的话,可以用树莓派 / Linux 命令行一步步分区 - 挂载 - 备份操作,大牛们以为想当然的事情小白查好久资料都整不明白,我也是试了好久好久才照着教程操作成功。</p><p>如果你是 Linux 大神的话,可以修改下上述 .sh 脚本文件中的参数,或者直接用我推荐的那个脚本,一键备份。更牛的可以封装成一个有图形界面的软件,造福全体小白。</p><p>以上就是关于树莓派系统备份的全部内容了,后续会继续更新在树莓派 4B 上搭建 WEB <a href="https://www.smzdm.com/fenlei/fuwuqi/">服务器</a>、NAS 云存储、软路由等内容,欢迎关注~</p><blockquote><p>参考</p><p><a href="https://blog.csdn.net/zhshh123/article/details/85063916">树莓派系统备份脚本(原理)</a></p><p><a href="https://blog.csdn.net/Chen_RuiMin/article/details/93903969">树莓派系统最小化备份(gparted)</a></p><p><a href="https://blog.csdn.net/zhufu86/article/details/78821056">手动一步一步来制作备份 Raspberry Pi 树莓派 SD 卡的 img 映像文件 (不用 dd 命令)</a></p><p><a href="https://blog.csdn.net/qingtian11112/article/details/99825257">制作树莓派 img 镜像文件(推荐脚本)</a></p></blockquote>]]></content>
<categories>
<category> Docker </category>
</categories>
<tags>
<tag> 教程 </tag>
<tag> 树莓派 </tag>
<tag> 笔记 </tag>
<tag> Ubuntu </tag>
</tags>
</entry>
<entry>
<title>【转载】Ubuntu20.04字符和图形界面切换</title>
<link href="/posts/61191.html"/>
<url>/posts/61191.html</url>
<content type="html"><![CDATA[<blockquote><p>【转载】 <a href="https://blog.csdn.net/Jailman/article/details/116301693">Ubuntu20.04 字符和图形界面切换</a></p></blockquote><p>设置默认开机模式</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 默认进入字符界面</span></span><br><span class="line">systemctl set-default multi-user.target</span><br><span class="line"><span class="comment"># 默认进入图形界面</span></span><br><span class="line">systemctl set-default graphical.target </span><br></pre></td></tr></table></figure><p>命令行界面进入图形界面</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">init 5</span><br></pre></td></tr></table></figure><p>图形界面切换命令行界面</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">CTRL+ALT+F1-F6</span><br></pre></td></tr></table></figure><p>关于 init 命令</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">Commands:</span><br><span class="line"> 0 Power-off the machine</span><br><span class="line"> 6 Reboot the machine</span><br><span class="line"> 2, 3, 4, 5 Start runlevelX.target unit</span><br><span class="line"> 1, s, S Enter rescue mode</span><br><span class="line"> q, Q Reload init daemon configuration</span><br><span class="line"> u, U Reexecute init daemon</span><br></pre></td></tr></table></figure>]]></content>
<categories>
<category> Docker </category>
</categories>
<tags>
<tag> 教程 </tag>
<tag> 笔记 </tag>
<tag> Ubuntu 20.04 </tag>
</tags>
</entry>
<entry>
<title>Ubuntu 20.04 或 Centos 7.4 安装 Transmission 下载器</title>
<link href="/posts/50304.html"/>
<url>/posts/50304.html</url>
<content type="html"><![CDATA[<h2 id="更新"><a href="#更新" class="headerlink" title="更新"></a>更新</h2><h3 id="2022-05-14"><a href="#2022-05-14" class="headerlink" title="2022-05-14"></a>2022-05-14</h3><p>增加 centos 7.4 下手动编译的安装方法,可以解决 yum 安装导致的权限问题。</p><h3 id="2022-05-13"><a href="#2022-05-13" class="headerlink" title="2022-05-13"></a>2022-05-13</h3><p>增加 centos 7.4 下安装方法</p><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>之前在群晖里一直用 Transmission 玩儿 PT,所以想在树莓派上也装一个玩儿玩儿。</p><blockquote><p><a href="https://transmissionbt.com/">Transmission</a>是一种BitTorrent客户端,特点是一个跨平台的后端和其上的简洁的用户界面。Transmission以MIT许可证和GNU通用公共许可证双许可证授权,因此是一款自由软件。挂PT的利器!</p></blockquote><h2 id="安装与配置"><a href="#安装与配置" class="headerlink" title="安装与配置"></a>安装与配置</h2><h3 id="直接用包管理软件安装就可以"><a href="#直接用包管理软件安装就可以" class="headerlink" title="直接用包管理软件安装就可以"></a>直接用包管理软件安装就可以</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># Ubuntu 20.04</span></span><br><span class="line">sudo apt-get install -y transmission-daemon</span><br><span class="line"><span class="comment"># Centos 7.4</span></span><br><span class="line">sudo yum install -y transmission-daemon</span><br></pre></td></tr></table></figure><h3 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h3><p>首先需要停止 transmission 的服务,不然会被修改的配置会被还原</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 先启动,会创建默认配置文件</span></span><br><span class="line">sudo systemctl start transmission-daemon.service</span><br><span class="line">sudo systemctl stop transmission-daemon.service</span><br></pre></td></tr></table></figure><p>修改 transmission 的配置文件</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># Ubuntu 20.04</span></span><br><span class="line">sudo vim /etc/transmission-daemon/settings.json</span><br><span class="line"><span class="comment"># Centos 7.4</span></span><br><span class="line">sudo vim /var/lib/transmission/.config/transmission-daemon/settings.json</span><br></pre></td></tr></table></figure><p>需要修改的地方已经做好了备注</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="attr">"alt-speed-down"</span>: <span class="number">50</span>,</span><br><span class="line"> <span class="attr">"alt-speed-enabled"</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">"alt-speed-time-begin"</span>: <span class="number">540</span>,</span><br><span class="line"> <span class="attr">"alt-speed-time-day"</span>: <span class="number">127</span>,</span><br><span class="line"> <span class="attr">"alt-speed-time-enabled"</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">"alt-speed-time-end"</span>: <span class="number">1020</span>,</span><br><span class="line"> <span class="attr">"alt-speed-up"</span>: <span class="number">50</span>,</span><br><span class="line"> <span class="attr">"bind-address-ipv4"</span>: <span class="string">"0.0.0.0"</span>, #开启IPV4地址绑定</span><br><span class="line"> <span class="attr">"bind-address-ipv6"</span>: <span class="string">"::"</span>, #开启IPV6地址绑定</span><br><span class="line"> <span class="attr">"blocklist-enabled"</span>: <span class="literal">false</span>, #启用黑名单</span><br><span class="line"> <span class="attr">"blocklist-url"</span>: <span class="string">"http://www.example.com/blocklist"</span>, #黑名单地址</span><br><span class="line"> <span class="attr">"cache-size-mb"</span>: <span class="number">4</span>,</span><br><span class="line"> <span class="attr">"dht-enabled"</span>: <span class="literal">false</span>, #DTH启用,这里我们要关闭</span><br><span class="line"> <span class="attr">"download-dir"</span>: <span class="string">"/home/jason/Downloads/PT"</span>, #默认下载目录</span><br><span class="line"> <span class="attr">"download-queue-enabled"</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">"download-queue-size"</span>: <span class="number">5</span>,</span><br><span class="line"> <span class="attr">"encryption"</span>: <span class="number">2</span>, #修改为2</span><br><span class="line"> <span class="attr">"idle-seeding-limit"</span>: <span class="number">30</span>,</span><br><span class="line"> <span class="attr">"idle-seeding-limit-enabled"</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">"incomplete-dir"</span>: <span class="string">"/var/lib/transmission/Downloads"</span>,</span><br><span class="line"> <span class="attr">"incomplete-dir-enabled"</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">"lpd-enabled"</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">"message-level"</span>: <span class="number">1</span>,</span><br><span class="line"> <span class="attr">"peer-congestion-algorithm"</span>: <span class="string">""</span>,</span><br><span class="line"> <span class="attr">"peer-id-ttl-hours"</span>: <span class="number">6</span>,</span><br><span class="line"> <span class="attr">"peer-limit-global"</span>: <span class="number">200</span>,</span><br><span class="line"> <span class="attr">"peer-limit-per-torrent"</span>: <span class="number">50</span>,</span><br><span class="line"> <span class="attr">"peer-port"</span>: <span class="number">51413</span>, #端口</span><br><span class="line"> <span class="attr">"peer-port-random-high"</span>: <span class="number">65535</span>,</span><br><span class="line"> <span class="attr">"peer-port-random-low"</span>: <span class="number">49152</span>,</span><br><span class="line"> <span class="attr">"peer-port-random-on-start"</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">"peer-socket-tos"</span>: <span class="string">"default"</span>,</span><br><span class="line"> <span class="attr">"pex-enabled"</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">"port-forwarding-enabled"</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">"preallocation"</span>: <span class="number">1</span>,</span><br><span class="line"> <span class="attr">"prefetch-enabled"</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">"queue-stalled-enabled"</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">"queue-stalled-minutes"</span>: <span class="number">30</span>,</span><br><span class="line"> <span class="attr">"ratio-limit"</span>: <span class="number">2</span>,</span><br><span class="line"> <span class="attr">"ratio-limit-enabled"</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">"rename-partial-files"</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">"rpc-authentication-required"</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">"rpc-bind-address"</span>: <span class="string">"0.0.0.0"</span>,</span><br><span class="line"> <span class="attr">"rpc-enabled"</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">"rpc-host-whitelist"</span>: <span class="string">""</span>,</span><br><span class="line"> <span class="attr">"rpc-host-whitelist-enabled"</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">"rpc-password"</span>: ,#密码,编辑的时候输入明文密码,保持之后,自动变为加密的</span><br><span class="line"> <span class="attr">"rpc-port"</span>: <span class="number">9091</span>,</span><br><span class="line"> <span class="attr">"rpc-url"</span>: <span class="string">"/transmission/"</span>,</span><br><span class="line"> <span class="attr">"rpc-username"</span>: <span class="string">"jason"</span>, #用户名</span><br><span class="line"> <span class="attr">"rpc-whitelist"</span>: <span class="string">"*"</span>, #白名单IP,多个IP用‘,'分隔, <span class="attr">"*"</span>表示允许所有</span><br><span class="line"> <span class="attr">"rpc-whitelist-enabled"</span>: <span class="literal">true</span>, #是否启用白名单,如果需要可以修改为true</span><br><span class="line"> <span class="attr">"scrape-paused-torrents-enabled"</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">"script-torrent-done-enabled"</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">"script-torrent-done-filename"</span>: <span class="string">""</span>,</span><br><span class="line"> <span class="attr">"seed-queue-enabled"</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">"seed-queue-size"</span>: <span class="number">10</span>,</span><br><span class="line"> <span class="attr">"speed-limit-down"</span>: <span class="number">100</span>,</span><br><span class="line"> <span class="attr">"speed-limit-down-enabled"</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">"speed-limit-up"</span>: <span class="number">100</span>,</span><br><span class="line"> <span class="attr">"speed-limit-up-enabled"</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">"start-added-torrents"</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">"trash-original-torrent-files"</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">"umask"</span>: <span class="number">18</span>,</span><br><span class="line"> <span class="attr">"upload-slots-per-torrent"</span>: <span class="number">14</span>,</span><br><span class="line"> <span class="attr">"utp-enabled"</span>: <span class="literal">true</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><blockquote><p>使用 yum 安装 transmission 时,下载目录不能指定到 <code>/home/user</code> 目录下,会有权限不足的问题,建议按照上面的默认选项设置。</p></blockquote><h3 id="权限"><a href="#权限" class="headerlink" title="权限"></a>权限</h3><p>把 transmission 的下载文件夹修改权限</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">sudo chmod 777 <文件夹></span><br></pre></td></tr></table></figure><h3 id="开机自启"><a href="#开机自启" class="headerlink" title="开机自启"></a>开机自启</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">sudo systemctl <span class="built_in">enable</span> transmission-daemon.service</span><br></pre></td></tr></table></figure><h3 id="centos-编译安装"><a href="#centos-编译安装" class="headerlink" title="centos 编译安装"></a>centos 编译安装</h3><h4 id="准备环境"><a href="#准备环境" class="headerlink" title="准备环境"></a>准备环境</h4><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">sudo yum update</span><br><span class="line">sudo yum groupinstall -y <span class="string">'development tools'</span></span><br><span class="line">sudo yum -y install gcc gcc-c++ m4 make automake libtool gettext openssl-devel libcurl-devel libevent-devel intltool gtk3-devel</span><br></pre></td></tr></table></figure><h4 id="下载源码包"><a href="#下载源码包" class="headerlink" title="下载源码包"></a>下载源码包</h4><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">wget https://github.com/transmission/transmission/releases/download/3.00/transmission-3.00.tar.xz</span><br><span class="line">tar -xf transmission-3.00.tar.xz</span><br><span class="line"><span class="built_in">cd</span> transmission-3.00</span><br></pre></td></tr></table></figure><h4 id="编译安装"><a href="#编译安装" class="headerlink" title="编译安装"></a>编译安装</h4><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">./configure</span><br><span class="line">make</span><br><span class="line">sudo make install</span><br></pre></td></tr></table></figure><h4 id="设定-systemd-Transmission-service"><a href="#设定-systemd-Transmission-service" class="headerlink" title="设定 systemd Transmission-service"></a>设定 systemd Transmission-service</h4><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">sudo vim /etc/systemd/system/transmission.service</span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">[Unit]</span><br><span class="line">Description=Transmission BitTorrent Daemon</span><br><span class="line">After=network.target</span><br><span class="line"></span><br><span class="line">[Service]</span><br><span class="line">User=root</span><br><span class="line">LimitNOFILE=100000</span><br><span class="line">ExecStart=/usr/<span class="built_in">local</span>/bin/transmission-daemon -f --log-error -g /usr/<span class="built_in">local</span>/share/transmission</span><br><span class="line"></span><br><span class="line">[Install]</span><br><span class="line">WantedBy=multi-user.target</span><br></pre></td></tr></table></figure><h4 id="创建默认配置"><a href="#创建默认配置" class="headerlink" title="创建默认配置"></a>创建默认配置</h4><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">sudo systemctl daemon-reload</span><br><span class="line">sudo systemctl <span class="built_in">enable</span> transmission.service</span><br><span class="line">sudo systemctl start transmission.service</span><br><span class="line">sudo systemctl stop transmission.service</span><br></pre></td></tr></table></figure><h4 id="修改配置"><a href="#修改配置" class="headerlink" title="修改配置"></a>修改配置</h4><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">sudo vim /usr/<span class="built_in">local</span>/share/transmission/settings.json</span><br></pre></td></tr></table></figure><p>具体配置参照上面的内容</p><p>重新启动</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">sudo systemctl start transmission.service</span><br></pre></td></tr></table></figure><h4 id="防火墙放行"><a href="#防火墙放行" class="headerlink" title="防火墙放行"></a>防火墙放行</h4><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">sudo firewall-cmd --permanent --add-port=9091/tcp</span><br><span class="line">sudo firewall-cmd --permanent --add-port=56789/tcp</span><br><span class="line">sudo firewall-cmd --permanent --add-port=56789/udp</span><br></pre></td></tr></table></figure><p>重启防火墙</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">sudo firewall-cmd --reload</span><br></pre></td></tr></table></figure><h2 id="美化"><a href="#美化" class="headerlink" title="美化"></a>美化</h2><p>原版的界面很丑,美化的方法如下,是大佬自定义的一套 WebUI</p><p><a href="https://github.com/ronggang/transmission-web-control">transmission-web-control</a></p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">wget https://gitee.com/styf3501/transmission-web-control/raw/master/install-tr-control-gitee.sh</span><br><span class="line">sudo chmod +x install-tr-control-gitee.sh</span><br><span class="line">sudo bash ./install-tr-control-gitee.sh</span><br></pre></td></tr></table></figure><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><ol><li><a href="https://github.com/ronggang/transmission-web-control">https://github.com/ronggang/transmission-web-control</a></li><li><a href="https://jtxiao.com/main/posts/ubuntu-20.04-%E5%AE%89%E8%A3%85-transmission/">https://jtxiao.com/main/posts/ubuntu-20.04-%E5%AE%89%E8%A3%85-transmission/</a></li><li><a href="https://wp.madjack.info/linux/transmission-2-92-centos-7.html">Transmission 2.92 ~2.94 編譯 for Centos 7</a></li></ol>]]></content>
<categories>
<category> 教程 </category>
</categories>
<tags>
<tag> 教程 </tag>
<tag> 配置 </tag>
</tags>
</entry>
<entry>
<title>Ubuntu 20.04 将阿里云盘挂载到本地并设置开机自动挂载</title>
<link href="/posts/43029.html"/>
<url>/posts/43029.html</url>
<content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>阿里云盘已经开启公测很久了,虽然大部分容量都不是永久的,但是依然有不可抵挡的诱惑——不限速。</p><p>Github 上已经有大神将阿里云盘转为 WebDAV 协议的文件服务器,仓库链接 <a href="https://github.com/zxbu/webdav-aliyundriver">在此</a></p><p>官方介绍</p><blockquote><p>本项目实现了阿里云盘的 webda v协议,只需要简单的配置一下,就可以让阿里云盘变身为 webdav 协议的文件服务器。 基于此,你可以把阿里云盘挂载为 Windows、Linux、Mac 系统的磁盘,可以通过 NAS 系统做文件管理或文件同步,更多玩法等你挖掘。</p></blockquote><h2 id="作为-Docker-容器部署"><a href="#作为-Docker-容器部署" class="headerlink" title="作为 Docker 容器部署"></a>作为 Docker 容器部署</h2><p>以下内容摘自项目官方教程</p><h3 id="获取必需参数-refreshToken"><a href="#获取必需参数-refreshToken" class="headerlink" title="获取必需参数 refreshToken"></a>获取必需参数 refreshToken</h3><ol><li>先通过浏览器(建议chrome)打开 <a href="https://www.aliyundrive.com/drive/">阿里云盘官网</a> 并登录</li><li>登录成功后,按 F12 打开开发者工具,点击 应用(Application),点击本地存储空间(Local Storage),点击 Local Storage 下的 <a href="https://www.aliyundrive.com/%EF%BC%8C%E7%82%B9%E5%87%BB%E5%8F%B3%E8%BE%B9%E7%9A%84">https://www.aliyundrive.com/,点击右边的</a> token,此时可以看到里面的数据,其中就有 refresh_token,把其值复制出来即可。格式为小写字母和数字,不要复制双引号,例子:ca6bf2175d73as2188efg81f87e55f11</li></ol><img src="https://user-images.githubusercontent.com/32785355/119246278-e6760880-bbb2-11eb-877c-aca16cf75d89.png" style="zoom: 67%;" /><h3 id="启动容器"><a href="#启动容器" class="headerlink" title="启动容器"></a>启动容器</h3><p>装好 Docker 后,输入一下命令部署</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">docker run -d --name=webdav-aliyundriver --restart=always -p 本机端口:8080 -v /etc/localtime:/etc/localtime -v /etc/aliyun-driver/:/etc/aliyun-driver/ -e TZ=<span class="string">"Asia/Shanghai"</span> -e ALIYUNDRIVE_REFRESH_TOKEN=<span class="string">"your refreshToken"</span> -e ALIYUNDRIVE_AUTH_PASSWORD=<span class="string">"admin"</span> -e JAVA_OPTS=<span class="string">"-Xmx1g"</span> zx5253/webdav-aliyundriver</span><br><span class="line"></span><br><span class="line"><span class="comment"># /etc/aliyun-driver/ 挂载卷自动维护了最新的 refreshToken,建议挂载</span></span><br><span class="line"><span class="comment"># ALIYUNDRIVE_AUTH_PASSWORD 是 admin 账户的密码,建议修改</span></span><br><span class="line"><span class="comment"># JAVA_OPTS 可修改最大内存占用,比如 -e JAVA_OPTS="-Xmx512m" 表示最大内存限制为 512m</span></span><br></pre></td></tr></table></figure><p>参数说明</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">--aliyundrive.refresh-token</span><br><span class="line"> 阿里云盘的 refreshToken</span><br><span class="line">--server.port</span><br><span class="line"> 非必填,服务器端口号,默认为8080</span><br><span class="line">--aliyundrive.auth.enable=<span class="literal">true</span></span><br><span class="line"> 是否开启WebDav账户验证,默认开启</span><br><span class="line">--aliyundrive.auth.user-name=admin</span><br><span class="line"> WebDav账户,默认admin</span><br><span class="line">--aliyundrive.auth.password=admin</span><br><span class="line"> WebDav密码,默认admin</span><br><span class="line">--aliyundrive.work-dir=/etc/aliyun-driver/</span><br><span class="line"> token挂载路径(如果多开的话,需修改此配置)</span><br></pre></td></tr></table></figure><h2 id="开机自动挂载"><a href="#开机自动挂载" class="headerlink" title="开机自动挂载"></a>开机自动挂载</h2><h3 id="前期准备"><a href="#前期准备" class="headerlink" title="前期准备"></a>前期准备</h3><ol><li><p>安装所需软件</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">sudo apt-get install davfs2 -y</span><br></pre></td></tr></table></figure></li><li><p>创建挂载目录</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">mkdir /home/ubuntu/alicloud</span><br></pre></td></tr></table></figure></li><li><p>挂载阿里云盘到本地</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">sudo mount -t davfs -o noexec localhost:端口/文件夹名 /home/ubuntu/alicloud</span><br></pre></td></tr></table></figure><p>端口就是刚刚启动容器时设置的本地端口,文件夹名就是要挂载到本地的阿里云盘的文件夹名,不设置文件夹就是默认所有文件夹。</p></li><li><p>按要求输入账户和密码,挂载成功后,即可当正常磁盘一样访问 WebDAV 服务了,速度快慢取决于网速。</p></li><li><p>输入 <code>df -h</code> 即可看到是否挂载成功</p><p><img src="https://images.961110.xyz:5001/i/2022/01/14/alicloud-.png" alt="image-20220114160206764"></p></li><li><p>解除挂载</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">sudo umount /home/ubuntu/alicloud</span><br></pre></td></tr></table></figure></li></ol><h3 id="设置开机自动挂载"><a href="#设置开机自动挂载" class="headerlink" title="设置开机自动挂载"></a>设置开机自动挂载</h3><h4 id="设置-WebDAV-免登陆密码"><a href="#设置-WebDAV-免登陆密码" class="headerlink" title="设置 WebDAV 免登陆密码"></a>设置 WebDAV 免登陆密码</h4><p>修改 /etc/davfs2/secrets,增加下面一行</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">localhost:端口/文件夹名 davusername davpassword</span><br></pre></td></tr></table></figure><h3 id="设置开机自动挂载-1"><a href="#设置开机自动挂载-1" class="headerlink" title="设置开机自动挂载"></a>设置开机自动挂载</h3><p>修改 /etc/fstab,增加下面一行</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">localhost:端口/文件夹名 /home/ubuntu/alicloud davfs rw,user,uid=username,auto 0 0</span><br></pre></td></tr></table></figure><p>uid 就是当前 Linux 用户名,不是 WebDAV 服务的用户名。</p><p>详细的挂载格式可以参考上一篇博文 <a href="https://blog.961110.xyz/posts/1946.html#%E4%BF%AE%E6%94%B9-fstab">Ubuntu 20.04 开机自动挂载 NTFS 格式存储设备</a></p><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><ol><li><a href="https://shipengliang.com/software-exp/%E5%A6%82%E4%BD%95%E5%9C%A8ubuntu%E6%88%96centos%E5%B0%86webdav%E6%8C%82%E8%BD%BD%E4%B8%BA%E6%9C%AC%E5%9C%B0%E7%A3%81%E7%9B%98.html">如何在Ubuntu或CentOS将WebDAV挂载为本地磁盘</a></li><li><a href="https://github.com/zxbu/webdav-aliyundriver">https://github.com/zxbu/webdav-aliyundriver</a></li></ol>]]></content>
<categories>
<category> 教程 </category>
</categories>
<tags>
<tag> 教程 </tag>
<tag> 配置 </tag>
</tags>
</entry>
<entry>
<title>Ubuntu 20.04 开机自动挂载 NTFS 格式存储设备</title>
<link href="/posts/1946.html"/>
<url>/posts/1946.html</url>
<content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>博主有一个无用的 U 盘想将其挂载到基于 Ubuntu Server 20.04 LTS 的树莓派 4B 中拓展存储。为了方便 PC 和树莓派两个平台都可以读写,所以格式化为 NTFS 格式(exFAT 格式也可以),并在树莓派中设置成开机自动挂载,方便使用。</p><h2 id="开机自动挂载方法"><a href="#开机自动挂载方法" class="headerlink" title="开机自动挂载方法"></a>开机自动挂载方法</h2><p>Ubuntu Server 20.04 LTS 默认就可以挂载 NTFS 格式的设备(至少在博主的设备上可以),无需安装其他软件。</p><h3 id="获取分区信息"><a href="#获取分区信息" class="headerlink" title="获取分区信息"></a>获取分区信息</h3><p>输入 <code>lsblk</code></p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT</span><br><span class="line">loop0 7:0 0 62M 1 loop /snap/lxd/21032</span><br><span class="line">loop1 7:1 0 48.9M 1 loop /snap/core18/2127</span><br><span class="line">loop2 7:2 0 28.1M 1 loop /snap/snapd/12707</span><br><span class="line">sda 8:0 1 28.9G 0 disk</span><br><span class="line">└─sda1 8:1 1 28.9G 0 part /home/ubunutu/storage</span><br><span class="line">mmcblk0 179:0 0 30G 0 disk</span><br><span class="line">├─mmcblk0p1 179:1 0 256M 0 part /boot/firmware</span><br><span class="line">└─mmcblk0p2 179:2 0 29.8G 0 part /</span><br></pre></td></tr></table></figure><p>可以看到 sda1 代表的就是这个 U 盘。</p><p>输入 <code>sudo blkid /dev/sda1</code></p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">/dev/sda1: LABEL=<span class="string">"storage"</span> UUID=<span class="string">"20466764466739A4"</span> TYPE=<span class="string">"ntfs"</span> PARTLABEL=<span class="string">"Basic data partition"</span> PARTUUID=<span class="string">"76ac87a1-aba5-4398-abbb-f49d1ce2c95c"</span></span><br></pre></td></tr></table></figure><p>可以看到格式为 ntfs,记录下 <code>UUID</code> 的值</p><h3 id="修改-fstab"><a href="#修改-fstab" class="headerlink" title="修改 fstab"></a>修改 fstab</h3><p>输入 <code>sudo vim /etc/fstab</code></p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">LABEL=writable / ext4defaults 01</span><br><span class="line">LABEL=system-boot /boot/firmwarevfat defaults 0 1</span><br></pre></td></tr></table></figure><p>按照这个格式添加自己的设备即可</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">格式:<fs spec><fs file><fs vfstype><fs mntops><fs freq><fs passno></span><br><span class="line"></span><br><span class="line"><fs spec>:分区定位,可以给UUID或LABEL,例如:UUID=20466764466739A4 或 LABEL=storage</span><br><span class="line"><fs file>:具体挂载点的位置,例如:/home/ubunutu/storage</span><br><span class="line"><fs vfstype>:挂载磁盘类型,根据实际情况填写,linux 分区一般为 ext4,windows 分区一般为 ntfs</span><br><span class="line"><fs mntops>:挂载参数,一般为defaults</span><br><span class="line"><fs freq>:磁盘检查,默认为0,需要开机检查磁盘则为1</span><br><span class="line"><fs passno>:引导选项,0不启动,1启动,2非启动,一般情况下只能为0或2</span><br></pre></td></tr></table></figure><p><strong>每一个选项之间,使用 Tab 而不是空格。</strong></p><p>所以可以增加一条配置</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">UUID=20466764466739A4 /home/ubunutu/storage ntfs defaults 0 2</span><br><span class="line"><span class="comment"># 或者</span></span><br><span class="line">LABEL=storage/home/ubunutu/storage ntfs defaults 0 2</span><br></pre></td></tr></table></figure><p>重启查看一下是否自动挂载到了指定位置。</p><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><ol><li><a href="https://lxnchan.cn/ubuntu-mount-disk.html">https://lxnchan.cn/ubuntu-mount-disk.html</a></li><li><a href="https://zhzh.xyz/2019/01/10/Linux/ubuntumountdiskdefault/">https://zhzh.xyz/2019/01/10/Linux/ubuntumountdiskdefault/</a></li></ol>]]></content>
<categories>
<category> 教程 </category>
</categories>
<tags>
<tag> 教程 </tag>
<tag> 配置 </tag>
</tags>
</entry>
<entry>
<title>树莓派 4B 安装 Ubuntu Server 20.04 LTS</title>
<link href="/posts/46533.html"/>
<url>/posts/46533.html</url>
<content type="html"><![CDATA[<h2 id="前期准备"><a href="#前期准备" class="headerlink" title="前期准备"></a>前期准备</h2><p>博主基于 Windows 10 进行的各项操作,Linux 平台请参阅其他博文。</p><h3 id="系统镜像"><a href="#系统镜像" class="headerlink" title="系统镜像"></a>系统镜像</h3><p>Ubuntu 官方有专门针对树莓派的发行版,树莓派 Ubuntu 镜像的链接 <a href="https://ubuntu.com/download/raspberry-pi">在此</a></p><p>提供了三个版本的镜像,系统镜像依次递减,按需选择。</p><ol><li>Ubuntu Desktop</li><li>Ubuntu Server</li><li>Ubuntu Core</li></ol><p>下载下来是一个压缩包,解压出 .img 文件即可。</p><h3 id="其他软件"><a href="#其他软件" class="headerlink" title="其他软件"></a>其他软件</h3><ol><li><p>SD卡格式化工具:SD Card Formatter <a href="https://www.sdcard.org/downloads/formatter/sd-memory-card-formatter-for-windows-download/">官网下载链接</a></p></li><li><p>写镜像工具</p><ul><li>Win32DiskImager <a href="https://sourceforge.net/projects/win32diskimager/files/latest/download">官方下载链接</a></li><li>Raspberry Pi Imager <a href="https://downloads.raspberrypi.org/imager/imager_latest.exe">官方下载链接</a></li></ul><p>两个都用过,在写镜像的时候官方的工具明显比 Win32DiskImager 慢,具体原因未知。</p></li></ol><h2 id="刷写系统"><a href="#刷写系统" class="headerlink" title="刷写系统"></a>刷写系统</h2><p>步骤和 <a href="https://blog.961110.xyz/posts/59454.html">树莓派4B启动配置</a> 一样,直接拷贝了过来。</p><h3 id="格式化SD卡"><a href="#格式化SD卡" class="headerlink" title="格式化SD卡"></a>格式化SD卡</h3><p>选择好SD卡对应的盘符,选择Quick format,点击右下角Format,稍等片刻即可。<strong>注意不要选择错了盘符,否则后果不堪设想。</strong></p><table> <tr> <td ><center><img src="https://images.961110.xyz:5001/i/2021/10/19/SD-Card-Formatter.png" ></center></td> <td ><center><img src="https://s2.loli.net/2022/01/12/QRiOvFPMKXjmplt.png" alt="image-20211016202211668" ></center></td> </tr></table><h3 id="刷写镜像"><a href="#刷写镜像" class="headerlink" title="刷写镜像"></a>刷写镜像</h3><p>选择刚刚解压出来的镜像文件,选择好盘符,然后点击写入即可,完成速度取决于SD卡及读卡器的速度。</p><table> <tr> <td ><center><img src="https://s2.loli.net/2022/01/12/OjJoAWr8kX5v2HK.png" > </center></td> <td ><center><img src="https://images.961110.xyz:5001/i/2021/10/19/1163840ac01b2da689eb0dc2686f4eb3.png" ></center></td> </tr></table><p>完成后如果找不到盘符,可以重新插拔读卡器,<strong>如果提示有未识别的分区,切勿格式化</strong>。</p><h2 id="启动"><a href="#启动" class="headerlink" title="启动"></a>启动</h2><h3 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h3><h4 id="通过网线共享电脑的网络"><a href="#通过网线共享电脑的网络" class="headerlink" title="通过网线共享电脑的网络"></a>通过网线共享电脑的网络</h4><blockquote><p>设置–>状态–>高级网络设置:更改适配器选项–>选择电脑联网的适配器–>右键选择属性,点击共享–>打开下图中的选项–>选择树莓派要连接的适配器–>确定</p></blockquote><img src="https://images.961110.xyz:5001/i/2021/10/19/2d3ddc0a200c6e2f6386bbfe718cb969.png" alt="image-20211017133126324" style="zoom:50%;" /><p>此种方式的默认的网关 IP 是 192.168.137.1,所以分配给树莓派的 IP 是 192.168.137.XXX ,具体查看方式见下文。</p><h4 id="通过配置WiFi上网"><a href="#通过配置WiFi上网" class="headerlink" title="通过配置WiFi上网"></a>通过配置WiFi上网</h4><p>打开 system-boot 分区名为 network-config 的文件,修改</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">version: 2</span><br><span class="line">ethernets:</span><br><span class="line"> eth0:</span><br><span class="line"> dhcp4: true</span><br><span class="line"> optional: true</span><br><span class="line">wifis:</span><br><span class="line"> wlan0:</span><br><span class="line"> dhcp4: true</span><br><span class="line"> optional: true</span><br><span class="line"> access-points:</span><br><span class="line"> "<network name>":</span><br><span class="line"> password: "<password>"</span><br></pre></td></tr></table></figure><p>需要注意缩进问题。</p><h3 id="查看树莓派-IP"><a href="#查看树莓派-IP" class="headerlink" title="查看树莓派 IP"></a>查看树莓派 IP</h3><h4 id="通过网线联网,查看树莓派Raspberry-4B的IP"><a href="#通过网线联网,查看树莓派Raspberry-4B的IP" class="headerlink" title="通过网线联网,查看树莓派Raspberry 4B的IP"></a>通过网线联网,查看树莓派Raspberry 4B的IP</h4><p>打开 cmd 或其他 shell 工具,通过<code>arp -a</code>查看本机的 arp 表,找到接口为 192.168.137.1 下的内容</p><p>可以看到树莓派 Raspberry 4B 获得的 IP: 192.168.137.XXX</p><h4 id="通过配置WiFi上网,查看树莓派Raspberry-4B的IP"><a href="#通过配置WiFi上网,查看树莓派Raspberry-4B的IP" class="headerlink" title="通过配置WiFi上网,查看树莓派Raspberry 4B的IP"></a>通过配置WiFi上网,查看树莓派Raspberry 4B的IP</h4><p>进入路由器后台,查看接入网络的设备列表,记录其 IP 即可。</p><p>此处博主通过 Windows 10 开启移动热点,树莓派连接上 WIFI 之后可以直接看到IP地址</p><table> <tr> <td ><center><img src="https://images.961110.xyz:5001/i/2021/10/21/Windows10851671f235bebad9.png" alt="image-20211021162217862" ></center></td> <td ><center><img src="https://images.961110.xyz:5001/i/2021/10/21/Windows104a0509f4c0915e31.png" ></center></td> </tr></table><h3 id="系统启动"><a href="#系统启动" class="headerlink" title="系统启动"></a>系统启动</h3><p>开机后需要等10分钟左右,如果没找到 IP ,不要慌,在确保配置无误的情况下,重启树莓派,再等 10 分钟左右去看一下,应该就有了。</p><h3 id="ssh连接"><a href="#ssh连接" class="headerlink" title="ssh连接"></a>ssh连接</h3><p>获取IP后,直接 <code>ssh ubuntu@IP</code> 即可</p><p>密码是 ubuntu,第一次登录会强制更改密码。</p><h2 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h2><h3 id="更换软件源"><a href="#更换软件源" class="headerlink" title="更换软件源"></a>更换软件源</h3><p>apt 软件源是 <code>/etc/apt/sources.list</code> 文件,修改 sources.list 前先做个备份,然后填入阿镜像源</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">deb http://mirrors.aliyun.com/ubuntu-ports/ focal main restricted universe multiverse</span><br><span class="line">deb-src http://mirrors.aliyun.com/ubuntu-ports/ focal main restricted universe multiverse</span><br><span class="line"></span><br><span class="line">deb http://mirrors.aliyun.com/ubuntu-ports/ focal-security main restricted universe multiverse</span><br><span class="line">deb-src http://mirrors.aliyun.com/ubuntu-ports/ focal-security main restricted universe multiverse</span><br><span class="line"></span><br><span class="line">deb http://mirrors.aliyun.com/ubuntu-ports/ focal-updates main restricted universe multiverse</span><br><span class="line">deb-src http://mirrors.aliyun.com/ubuntu-ports/ focal-updates main restricted universe multiverse</span><br><span class="line"></span><br><span class="line">deb http://mirrors.aliyun.com/ubuntu-ports/ focal-proposed main restricted universe multiverse</span><br><span class="line">deb-src http://mirrors.aliyun.com/ubuntu-ports/ focal-proposed main restricted universe multiverse</span><br><span class="line"></span><br><span class="line">deb http://mirrors.aliyun.com/ubuntu-ports/ focal-backports main restricted universe multiverse</span><br><span class="line">deb-src http://mirrors.aliyun.com/ubuntu-ports/ focal-backports main restricted universe multiverse</span><br></pre></td></tr></table></figure><p>清华大学镜像源 <a href="https://mirrors.tuna.tsinghua.edu.cn/help/ubuntu-ports/">在此</a></p><p>更换完毕后,输入</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">sudo apt update</span><br><span class="line">sudo apt upgrade</span><br></pre></td></tr></table></figure><h3 id="将-python-默认设置为-python3"><a href="#将-python-默认设置为-python3" class="headerlink" title="将 python 默认设置为 python3"></a>将 python 默认设置为 python3</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">sudo apt install python-is-python3 python-dev-is-python3</span><br></pre></td></tr></table></figure><h3 id="安装-Cockpit-管理面板"><a href="#安装-Cockpit-管理面板" class="headerlink" title="安装 Cockpit 管理面板"></a>安装 Cockpit 管理面板</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">sudo apt -y install cockpit</span><br></pre></td></tr></table></figure><p>然后浏览器打开 <strong>服务器IP:9090</strong> 即可访问管理面板</p><p>在这里可以直观的看到 Ubuntu 的运行状态,包括 CPU 使用率、内存使用率、硬盘使用率等。</p><p>详细介绍以及如何设置开机自启可以参考之前的博文 <a href="https://blog.961110.xyz/posts/29824.html">使用 Cockpit 管理你的树莓派</a></p><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><ol><li><a href="https://blog.icexmoon.xyz/archives/138.html">https://blog.icexmoon.xyz/archives/138.html</a></li><li><a href="https://zhuanlan.zhihu.com/p/140952829">https://zhuanlan.zhihu.com/p/140952829</a></li><li><a href="https://blog.os7blue.com/article/169">https://blog.os7blue.com/article/169</a></li></ol>]]></content>
<categories>
<category> 教程 </category>
</categories>
<tags>
<tag> 教程 </tag>
<tag> 树莓派 </tag>
<tag> 配置 </tag>
</tags>
</entry>
<entry>
<title>【转载】解除 GitHub 仓库的 Fork 连接</title>
<link href="/posts/2377.html"/>
<url>/posts/2377.html</url>
<content type="html"><![CDATA[<blockquote><p>原文地址 <a href="https://miroox.github.io/blog/2018/05/2018-5-19-RemoveGitHubFork/">解除 GitHub 仓库的 Fork 连接</a></p></blockquote><p>GitHub 的 Fork 功能实际上主要是服务于 Pull Request,然而我以前并没有正确地认识到这一点,简单地把 Fork 当 clone 使,导致有些时候不太方便。比如生成这个博客的 <a href="https://github.com/miRoox/miRoox.github.io">仓库</a>,以前是直接 Fork <a href="https://github.com/leopardpan/leopardpan.github.io">leopardpan 的博客</a> 得到的。现在我想解除这种 Fork 关系,但同时保留提交的历史。然而在网上却没有找到有关的教程,没办法,只能自己试试。</p><p>实际做起来却意外的简单。</p><p>首先,把删去所有与本地仓库连接的远程仓库</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">$ git remote remove origin</span><br><span class="line">$ git remote remove upstream</span><br></pre></td></tr></table></figure><p>然后,把 GitHub 上自己的远程仓库给删了。</p><p>最后,在 GitHub 上重新建立同名仓库,然后把本地仓库关联上去。</p><figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">$ git remote add origin <your repo link></span><br></pre></td></tr></table></figure><p>于是就大功告成了。</p>]]></content>
<categories>
<category> 教程 </category>
</categories>
<tags>
<tag> 教程 </tag>
</tags>
</entry>
<entry>
<title>【转载】树莓派 SD 卡无损扩容方案——克隆到更大容量的SD卡</title>
<link href="/posts/11241.html"/>
<url>/posts/11241.html</url>
<content type="html"><![CDATA[<blockquote><p>原文地址 <a href="https://www.jianshu.com/p/a5cfbba49a68">超简单的树莓派SD卡扩容方案,将树莓派16GB的SD卡克隆到64GB的SD卡</a></p></blockquote><p>这段时间,往树莓派装了几个 Docker 镜像之后,16GB 的 SD 卡明显不够用了,于是我打算扩容一下,为了避免从零开始重做系统,我找到了完美克隆 16GB 的 SD 卡 按文件系统结构原样复制到 64GB 卡的方法。以下是具体步骤~</p><h2 id="前期准备"><a href="#前期准备" class="headerlink" title="前期准备"></a>前期准备</h2><ul><li> 一台可以同时读写两张 SD 的 Windows 电脑</li><li> 软件 DiskGenius</li></ul><h3 id="在树莓派查看旧-SD-卡容量"><a href="#在树莓派查看旧-SD-卡容量" class="headerlink" title="在树莓派查看旧 SD 卡容量"></a>在树莓派查看旧 SD 卡容量</h3><p>查看旧 SD 卡容量 <code>sudo fdisk -l</code></p><p><img src="https://images.961110.xyz:5001/i/2021/12/17/fdisk--l.png"></p><h3 id="将树莓派关机"><a href="#将树莓派关机" class="headerlink" title="将树莓派关机"></a>将树莓派关机</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">poweroff</span><br></pre></td></tr></table></figure><p>待 ssh 断开后,等待树莓派灯变红,关闭电源,抽出 SD 卡</p><h3 id="开启-DiskGenius"><a href="#开启-DiskGenius" class="headerlink" title="开启 DiskGenius"></a>开启 DiskGenius</h3><p>绿色版地址:<code>https://frp.v2fy.com/%E6%95%B0%E6%8D%AE%E6%81%A2%E5%A4%8D%E8%BD%AF%E4%BB%B6DiskGenius/DiskGenius.zip</code></p><ul><li> 解压后双击运行即可</li></ul><img src="https://images.961110.xyz:5001/i/2021/12/17/3203841-83e0235fd770e26d.png" style="zoom: 67%;" /><h3 id="将新旧两张-SD-卡插入电脑并读取"><a href="#将新旧两张-SD-卡插入电脑并读取" class="headerlink" title="将新旧两张 SD 卡插入电脑并读取"></a>将新旧两张 SD 卡插入电脑并读取</h3><table> <tr> <td ><center><img src="https://images.961110.xyz:5001/i/2021/12/17/16-GB.png" >图1 旧的 16GB 已有数据卡 </center></td> <td ><center><img src="https://images.961110.xyz:5001/i/2021/12/17/64GB-.png" >图2 新的 64GB 卡</center></td> </tr></table>克隆 SD 卡------------<h3 id="打开硬盘工具"><a href="#打开硬盘工具" class="headerlink" title="打开硬盘工具"></a>打开硬盘工具</h3><p>左上角点击工具 ==> 克隆硬盘</p><h3 id="选择源硬盘和目标硬盘"><a href="#选择源硬盘和目标硬盘" class="headerlink" title="选择源硬盘和目标硬盘"></a>选择源硬盘和目标硬盘</h3><table> <tr> <td ><center><img src="https://images.961110.xyz:5001/i/2021/12/17/98fa9fe245b7036a4d38fadc16267ad5.png" ></center></td> <td ><center><img src="https://images.961110.xyz:5001/i/2021/12/17/bac0e91af2c23d1925114f8269b35929.png" style="zoom: 67%"></center></td> </tr></table><h3 id="开始克隆"><a href="#开始克隆" class="headerlink" title="开始克隆"></a>开始克隆</h3><table> <tr> <td ><center><img src="https://images.961110.xyz:5001/i/2021/12/17/41e1eb2fcaec137c4dd69f6c2a423a4f.png" >图1 确认 </center></td> <td ><center><img src="https://images.961110.xyz:5001/i/2021/12/17/a10afd5d5ed83160c52133550a10eb60.png" >图2 建立新签名</center></td> </tr></table><table> <tr> <td ><center><img src="https://images.961110.xyz:5001/i/2021/12/17/ede6d149b43a10fc3986a8370e5847d3.png" >图1 等待 </center></td> <td ><center><img src="https://images.961110.xyz:5001/i/2021/12/17/2aca40e9c6e8122d0b49b1d65df1a39d.png" >图2 完成</center></td> </tr></table><img src="https://images.961110.xyz:5001/i/2021/12/17/fd65ba0ed1a774de9126401805566ae0.png" style="zoom: 50%;" /><h2 id="将新卡插入树莓派,开机"><a href="#将新卡插入树莓派,开机" class="headerlink" title="将新卡插入树莓派,开机"></a>将新卡插入树莓派,开机</h2><p>输入 <code>sudo fdisk -l</code></p><img src="https://images.961110.xyz:5001/i/2021/12/17/33846620bd20a90f27e63d227fe83e3b.png" style="zoom:67%;" /><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>树莓派 4B 可以选配 8GB 内存,配上大空间的 SD 空间也显得合理,如果你的树莓派 SD 空间不够用了,欢迎用本文提供的方法扩容,真的是省时省力,如果你想定期为树莓派 SD 卡做备份,本文的方法也是极好的选择。</p><p>如果你有多个同型号的树莓派,用本文的方法克隆旧 SD 卡到其他树莓派的 SD 卡,也能省去大量的树莓派配置时间。</p>]]></content>
<categories>
<category> 教程 </category>
</categories>
<tags>
<tag> 教程 </tag>
<tag> 树莓派 </tag>
<tag> 配置 </tag>
</tags>
</entry>
<entry>
<title>使用 dlv Debug dockerd(Docker Daemon) 的方法</title>
<link href="/posts/20654.html"/>
<url>/posts/20654.html</url>
<content type="html"><![CDATA[<h2 id="使用-dlv-来-Debug-dockerd-Docker-Daemon"><a href="#使用-dlv-来-Debug-dockerd-Docker-Daemon" class="headerlink" title="使用 dlv 来 Debug dockerd(Docker Daemon)"></a>使用 dlv 来 Debug dockerd(Docker Daemon)</h2><h3 id="编译-Docker-Daemon"><a href="#编译-Docker-Daemon" class="headerlink" title="编译 Docker Daemon"></a>编译 Docker Daemon</h3><p>编译 Docker Daemon 的两种方式已经在 <a href="https://blog.961110.xyz/posts/21885.html">前面的博文</a> 中比较详细的说明了。</p><h3 id="安装-dlv"><a href="#安装-dlv" class="headerlink" title="安装 dlv"></a>安装 dlv</h3><p>安装</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">go get -u github.com/go-delve/delve/cmd/dlv</span><br></pre></td></tr></table></figure><p>验证是否安装成功,输入 <code>dlv version</code>,安装成功会有如下输出。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">Delve Debugger</span><br><span class="line">Version: 1.7.3</span><br><span class="line">Build: <span class="variable">$Id</span>: c532746bc713b05a49680bc8c36b27c72c391a6b $</span><br></pre></td></tr></table></figure><h3 id="编译可-Debug-的-dockerd"><a href="#编译可-Debug-的-dockerd" class="headerlink" title="编译可 Debug 的 dockerd"></a>编译可 Debug 的 dockerd</h3><p>在之前的博文中,没有考虑到编译出的 Docker Daemon 需要调试,所以在用 dlv debug 的时候会报错。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">could not get .debug_frame section: could not find .debug_frame section</span><br></pre></td></tr></table></figure><p>默认情况下在编译 dockerd 的时候会有 <code>-w</code> 参数来关闭 DWARP 调试信息,这样就无法在二进制文件上使用 gdb 的特定功能如设置断点获取堆栈跟踪信息。所以在需要在编译脚本中增加 <code>DOCKER_DEBUG=true</code>,这样在编译时会自动删除 ldflags 中的 <code>-w</code> 参数,具体的修改位置是 <code>/home/jason/GOPATH/src/github.com/docker/docker/hack/make.sh:30</code></p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line">...</span><br><span class="line">set -o pipefail</span><br><span class="line"></span><br><span class="line">export DOCKER_PKG='github.com/docker/docker'</span><br><span class="line">export SCRIPTDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"</span><br><span class="line">export MAKEDIR="$SCRIPTDIR/make"</span><br><span class="line">export PKG_CONFIG=${PKG_CONFIG:-pkg-config}</span><br><span class="line"><span class="meta">#</span><span class="bash"> 新增的一行</span></span><br><span class="line">export DOCKER_DEBUG=true</span><br><span class="line"></span><br><span class="line">echo</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>在 Goland 的远程配置中,根据提示有两种方法来调试 Docker Daemon</p><img src="https://images.961110.xyz:5001/i/2021/12/16/Go-Remote-.png" alt="image-20211216200604227" style="zoom:50%;" /><ol><li>第一种方式是使用 dlv 进而编译,但是需要配置一大堆参数,不太方便。</li><li>第二种方式需要在编译时增加 <code>-gcflags</code> 参数,对应的修改位置是 <code>/home/jason/GOPATH/src/github.com/docker/docker/hack/make/.binary:87</code></li></ol><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">go build \</span><br><span class="line">-o <span class="string">"<span class="variable">$DEST</span>/<span class="variable">$BINARY_FULLNAME</span>"</span> \</span><br><span class="line"><span class="string">"<span class="variable">${BUILDFLAGS[@]}</span>"</span> \</span><br><span class="line"><span class="comment"># 增加的一行参数</span></span><br><span class="line">-gcflags <span class="string">"all=-N -l"</span> \</span><br><span class="line">-ldflags <span class="string">"</span></span><br><span class="line"><span class="string"><span class="variable">$LDFLAGS</span></span></span><br><span class="line"><span class="string"><span class="variable">$LDFLAGS_STATIC_DOCKER</span></span></span><br><span class="line"><span class="string"><span class="variable">$DOCKER_LDFLAGS</span></span></span><br><span class="line"><span class="string">"</span> \</span><br><span class="line"><span class="variable">${GO_PACKAGE}</span></span><br></pre></td></tr></table></figure><p>修改完成后,在 <code>Docker 项目根目录</code> 输入下面命令来手动构建 dockerd 二进制文件。</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">VERSION=test-build DOCKER_GITCOMMIT=1 ./hack/make.sh binary</span><br></pre></td></tr></table></figure><p>构建完成后 cd 到 <code>Docker 项目根目录/bundles/binary-daemon</code> 就可以看到刚刚编译出的文件 dockerd-test-build,并且已经做了软链接。</p><p>理所当然的构建出的 dockerd 会增加一定的体积</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 修改前</span></span><br><span class="line">总用量 62M</span><br><span class="line">drwxr-xr-x 2 jason root 4.0K 12月 16 19:46 .</span><br><span class="line">drwxr-xr-x 3 jason root 4.0K 12月 16 19:46 ..</span><br><span class="line">lrwxrwxrwx 1 jason root 19 12月 16 19:46 dockerd -> dockerd-jason-build</span><br><span class="line">-rwxr-xr-x 1 jason root 62M 12月 16 19:46 dockerd-jason-build</span><br><span class="line">-rw-r--r-- 1 jason root 54 12月 16 19:46 dockerd-jason-build.md5</span><br><span class="line">-rw-r--r-- 1 jason root 86 12月 16 19:46 dockerd-jason-build.sha256</span><br><span class="line"></span><br><span class="line"><span class="comment"># 修改后</span></span><br><span class="line">总用量 79M</span><br><span class="line">drwxr-xr-x 2 jason root 4.0K 12月 16 19:50 .</span><br><span class="line">drwxr-xr-x 3 jason root 4.0K 12月 16 19:50 ..</span><br><span class="line">lrwxrwxrwx 1 jason root 19 12月 16 19:50 dockerd -> dockerd-jason-build</span><br><span class="line">-rwxr-xr-x 1 jason root 79M 12月 16 19:50 dockerd-jason-build</span><br><span class="line">-rw-r--r-- 1 jason root 54 12月 16 19:50 dockerd-jason-build.md5</span><br><span class="line">-rw-r--r-- 1 jason root 86 12月 16 19:50 dockerd-jason-build.sha256</span><br></pre></td></tr></table></figure><h3 id="利用-dlv-开始-debug"><a href="#利用-dlv-开始-debug" class="headerlink" title="利用 dlv 开始 debug"></a>利用 dlv 开始 debug</h3><p>使用下面的命令启动 dockerd,就可以 debug 了</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">sudo <span class="variable">$GOPATH</span>/bin/dlv --headless=<span class="literal">true</span> --api-version=2 --<span class="built_in">log</span> --listen=:2345 <span class="built_in">exec</span> ./dockerd -- -D -H unix:// --containerd=/run/containerd/containerd.sock</span><br></pre></td></tr></table></figure><p><strong>–headless=true 表示不要命令界面</strong></p><p>或者使用下面的脚本来 debug 已经在运行的 dockerd</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash">!/bin/bash</span></span><br><span class="line"></span><br><span class="line">echo "Start to execute dlv_debug script"</span><br><span class="line">echo "==========================================================="</span><br><span class="line">echo</span><br><span class="line"></span><br><span class="line">pid=$(ps -ef | grep 'docker' | grep 'dockerd' | grep -v grep | awk '{print $2}')</span><br><span class="line"></span><br><span class="line">echo "pid of dockerd is $pid"</span><br><span class="line">echo</span><br><span class="line">echo '你的密码' | sudo -S $GOPATH/bin/dlv attach $pid --headless=true --api-version=2 --log --listen=:2345</span><br></pre></td></tr></table></figure><p>dlv 具体的使用方法可以参考其他博主的资料,博文下方有 <a href="http://blog.961110.xyz/posts/20654.html#%E5%8F%82%E8%80%83%E9%93%BE%E6%8E%A5">参考链接</a>。</p><h3 id="其他问题"><a href="#其他问题" class="headerlink" title="其他问题"></a>其他问题</h3><p>构建后使用 <code>docker run hello-world</code> 来进行验证,镜像移植拉不下来,错误 log 如下</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">error pulling image configuration: Get https://production.cloudflare.docker.com/registry-</span><br><span class="line">v2/docker/registry/v2/blobs/sha256/d2/d23bdf5b1b1b1afce5f1d0fd33e7ed8afbc084b594b9ccf742a5b27080d8</span><br><span class="line">a4a8/data?verify=1596513158-78B0ocrR%2Bn4iMvUrPrVx12jrGX8%3D: dial tcp 104.18.122.25:443: i/o timeout</span><br></pre></td></tr></table></figure><p>原因是没有配置 docker 源</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 没有的话就新建一个</span></span><br><span class="line">vim /etc/docker/daemon.json</span><br><span class="line"></span><br><span class="line"><span class="comment"># 加入以下内容</span></span><br><span class="line">{</span><br><span class="line"> <span class="string">"registry-mirrors"</span>: [</span><br><span class="line"> <span class="string">"https://hub-mirror.c.163.com"</span>,</span><br><span class="line"> <span class="string">"https://registry.aliyuncs.com"</span>,</span><br><span class="line"> <span class="string">"https://registry.docker-cn.com"</span>,</span><br><span class="line"> <span class="string">"https://docker.mirrors.ustc.edu.cn"</span></span><br><span class="line"> ]</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><ol><li><a href="https://liqiang.io/post/remote-debug-go-with-delve-bea7abce">Golang 调试工具 Delve</a></li><li><a href="https://zhuanlan.zhihu.com/p/364346530">Dlv 深入探究</a></li><li><a href="http://www.4k8k.xyz/article/nangonghen/114602755">源码编译docker-ce v18.09.2</a></li><li><a href="https://blog.csdn.net/elvishehai/article/details/107782762">docker run 报错: dial tcp xxxxxxxx:443: i/o timeout报错</a></li><li><a href="https://blog.csdn.net/cyq6239075/article/details/103911098">go build 命令参数详解</a></li><li><a href="https://blog.csdn.net/u010525694/article/details/103111375">dlv远程调试golang程序注意事项</a></li><li><a href="https://github.com/go-delve/delve">delve 官方 Github 链接</a></li><li><a href="https://blog.abnerzhao.com/posts/go-build/">go build 编译优化</a></li><li><a href="https://blog.csdn.net/u013536232/article/details/104123861?spm=1001.2101.3001.6650.5&utm_medium=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromBaidu~default-5.highlightwordscore&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromBaidu~default-5.highlightwordscore">goland远程调试</a></li><li><a href="https://linux.cn/article-12400-1.html">使用 Delve 代替 Println 来调试 Go 程序</a></li></ol>]]></content>
<categories>
<category> Docker </category>
</categories>
<tags>
<tag> Docker </tag>
<tag> 笔记 </tag>
<tag> 毕设 </tag>
<tag> 调试 </tag>
</tags>
</entry>
<entry>
<title>【转载】Linux 下 golang 多版本管理器</title>
<link href="/posts/21410.html"/>
<url>/posts/21410.html</url>
<content type="html"><![CDATA[<blockquote><p>原文地址 <a href="https://segmentfault.com/a/1190000019218168">golang 多版本管理器</a></p></blockquote><p><a href="https://link.segmentfault.com/?enc=HGrnxFltecJpZeDIol6ehg==.m1HIrix1OGH/YSkWTzlvyYCMHUI6dAj/Dd5UY7UZjp8=">g</a> 是一个 Linux、macOS、Windows 下的命令行工具,可以提供一个便捷的多版本 <a href="https://link.segmentfault.com/?enc=IiyYZQxxLv1Bxhk2ucPTQg==.DXW2ePczTCYlJBFgRMHx27dSg+tkmQn1stAu5SUZoVw=">go</a> 环境的管理和切换。</p><h2 id="特性"><a href="#特性" class="headerlink" title="特性"></a>特性</h2><ul><li> 支持列出可供安装的 go 版本号</li><li> 支持列出已安装的 go 版本号</li><li> 支持在本地安装多个 go 版本</li><li> 支持卸载已安装的 go 版本</li><li> 支持在已安装的 go 版本之间自由切换</li></ul><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><h3 id="自动化安装"><a href="#自动化安装" class="headerlink" title="自动化安装"></a>自动化安装</h3><ul><li><p>Linux/macOS(适用于 bash、zsh)</p> <figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 建议安装前清空`GOROOT`、`GOBIN`等环境变量</span></span><br><span class="line">$ wget -qO- https://raw.githubusercontent.com/voidint/g/master/install.sh | bash</span><br><span class="line">$ <span class="built_in">echo</span> <span class="string">"unalias g"</span> >> ~/.bashrc <span class="comment"># 可选。若其他程序(如'git')使用了'g'作为别名。</span></span><br><span class="line">$ <span class="built_in">source</span> ~/.bashrc <span class="comment"># 或者 source ~/.zshrc</span></span><br></pre></td></tr></table></figure></li></ul><h3 id="手动安装"><a href="#手动安装" class="headerlink" title="手动安装"></a>手动安装</h3><ul><li><p> 下载对应平台的<a href="https://link.segmentfault.com/?enc=1prd848WenP4l5j4FgFq5w==.mBCUO/9ZDtMAf0MIaGfmljqdRHlA0KMyZoRySI96DBuwp4N7/esQ11/PWJWlkGfD">二进制压缩包</a>。</p></li><li><p> 将压缩包解压至<code>PATH</code>环境变量目录下,如<code>/usr/local/bin</code>。</p></li><li><p>编辑 shell 环境配置文件(<code>~/.bashrc</code>、<code>~/.zshrc</code>…)</p> <figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ cat>>~/.bashrc<<<span class="string">EOF</span></span><br><span class="line"><span class="string">export GOROOT="${HOME}/.g/go"</span></span><br><span class="line"><span class="string">export PATH="${HOME}/.g/go/bin:$PATH"</span></span><br><span class="line"><span class="string">export G_MIRROR=https://golang.google.cn/dl/</span></span><br><span class="line"><span class="string">EOF</span></span><br></pre></td></tr></table></figure></li></ul><h2 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h2><p>查询当前可供安装的<code>stable</code>状态的 go 版本</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ g ls-remote stable</span><br><span class="line">1.11.9</span><br><span class="line">1.12.4</span><br></pre></td></tr></table></figure><p>安装目标 go 版本<code>1.12.4</code></p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ g install 1.12.4</span><br><span class="line">Installed successfully</span><br><span class="line">$ go version</span><br><span class="line">go version go1.12.4 darwin/amd64</span><br></pre></td></tr></table></figure><p>查询已安装的 go 版本</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ g ls</span><br><span class="line">1.12.4</span><br></pre></td></tr></table></figure><p>查询可供安装的所有 go 版本</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ g ls-remote</span><br><span class="line">1.2.2</span><br><span class="line">1.3</span><br><span class="line">1.3.1</span><br><span class="line">... // 省略若干版本</span><br><span class="line">1.11.7</span><br><span class="line">1.11.8</span><br><span class="line">1.11.9</span><br><span class="line">1.12</span><br><span class="line">1.12.1</span><br><span class="line">1.12.2</span><br><span class="line">1.12.3</span><br><span class="line">1.12.4</span><br></pre></td></tr></table></figure><p>安装目标 go 版本<code>1.11.9</code></p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ g install 1.11.9</span><br><span class="line">Installed successfully</span><br><span class="line">$ go version</span><br><span class="line">go version go1.11.9 darwin/amd64</span><br></pre></td></tr></table></figure><p>切换到另一个已安装的 go 版本</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ g ls</span><br><span class="line">1.11.9</span><br><span class="line">1.12.4</span><br><span class="line">$ g use 1.12.4</span><br><span class="line">go version go1.12.4 darwin/amd64</span><br></pre></td></tr></table></figure><p>卸载一个已安装的 go 版本</p><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">g uninstall 1.11.9</span><br><span class="line">Uninstall successfully</span><br></pre></td></tr></table></figure><h3 id="将-GOPATH-bin-添加到路径中"><a href="#将-GOPATH-bin-添加到路径中" class="headerlink" title="将 GOPATH/bin 添加到路径中"></a>将 GOPATH/bin 添加到路径中</h3><p>在 ~/.bashrc 或 ~/.zshrc 中增加下面几行</p><figure class="highlight shell"><table><tr><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> GO</span></span><br><span class="line">export GOPATH=$HOME/go</span><br><span class="line">export PATH=$PATH:$GOROOT/bin:$GOPATH/bin</span><br></pre></td></tr></table></figure><p>GOPATH 和 GOROOT 都使用 go env 这个命令列出,修改对应路径即可。</p><h2 id="FAQ"><a href="#FAQ" class="headerlink" title="FAQ"></a>FAQ</h2><ul><li><p>环境变量<code>G_MIRROR</code>有什么作用?</p><p> 由于中国大陆无法自由访问 Golang 官网,导致查询及下载 go 版本都变得困难,因此可以通过该环境变量指定一个镜像站点(如<code>https://golang.google.cn/dl/</code>),g 将从该站点查询、下载可用的 go 版本。</p></li><li><p>支持源代码编译安装吗?</p><p> 不支持</p></li></ul><h2 id="鸣谢"><a href="#鸣谢" class="headerlink" title="鸣谢"></a>鸣谢</h2><p>感谢 <a href="https://link.segmentfault.com/?enc=N4GmxCVmZsTbV6ul2dbu6A==.ibICcM7AsQynCM9wvSSnd0hyzBSz7cUkXf2H0GnB2Uo=">nvm</a>、<a href="https://link.segmentfault.com/?enc=ozQgme1W/tphsjjlX6NUsQ==.rsDgyfwAd7LfvIObBX2T4Ip7seZDRvdZhVXlgJBXRQU=">n</a>、<a href="https://link.segmentfault.com/?enc=FZndRW4e132UcOeEC1dI4g==.Qf1sRTAYRHRpU1qlvz8KrZMKdR32Uc+Mb/EXu/0sg1M=">rvm</a> 等工具提供的宝贵思路。</p>]]></content>
<categories>
<category> 教程 </category>
</categories>
<tags>
<tag> 教程 </tag>
<tag> golang </tag>
</tags>
</entry>
<entry>
<title>解决 Windows 10 电池模式下屏幕亮度低色彩泛白的问题</title>
<link href="/posts/2083.html"/>
<url>/posts/2083.html</url>
<content type="html"><![CDATA[<h2 id="前情提要"><a href="#前情提要" class="headerlink" title="前情提要"></a>前情提要</h2><p>博主用的电脑是美帝良心想的小新 Pro 14 Intel 1135G7 带 MX450 独显的版本,后期自己安装了 Windows 10 专业版,但是发现在不插电源使用电池的情况下,屏幕的亮度会变低而且会泛白,手动将亮度调低再调到最高可以暂时解决问题,但是前面的问题会复现。</p><h2 id="Intel-版解决办法"><a href="#Intel-版解决办法" class="headerlink" title="Intel 版解决办法"></a>Intel 版解决办法</h2><p>左下角搜索英特尔显卡控制中心。</p><table> <tr> <td ><center><img src="https://images.961110.xyz:5001/i/2021/12/07/602d06e942c9ac1b8e93a1b3e37725e3.png" >图1 搜索因特尔 </center></td> <td ><center><img src="https://images.961110.xyz:5001/i/2021/12/06/1bf9cddb5b2938df966452ff19fee278.png" style="zoom:50%;" >图2 英特尔显卡控制中心</center></td> </tr></table><p>打开系统界面,<strong>关闭显示器节能的选项,关闭面板自刷新选项</strong>。</p><img src="https://images.961110.xyz:5001/i/2021/12/07/48bd8d03592568d99fbe608353d0c5c1.png" alt="image-20211207111254144" style="zoom:50%;" /><p>通过上面的方法应该可以解决问题。</p><h2 id="AMD-版解决办法"><a href="#AMD-版解决办法" class="headerlink" title="AMD 版解决办法"></a>AMD 版解决办法</h2><p>AMD 版解决办法来源于 <a href="https://blog.csdn.net/m0_46552496/article/details/119822436">联想笔记本拔掉电源后屏幕发白、泛白,对比度降低</a> ,就不再搬运了。</p><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><ol><li><a href="https://blog.csdn.net/m0_46552496/article/details/119822436">联想笔记本拔掉电源后屏幕发白、泛白,对比度降低</a></li></ol>]]></content>
<categories>
<category> 教程 </category>
</categories>
<tags>
<tag> 教程 </tag>
<tag> Windows 10 </tag>
</tags>
</entry>
<entry>
<title>Docker 日志管理机制</title>
<link href="/posts/53979.html"/>
<url>/posts/53979.html</url>
<content type="html"><![CDATA[<h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>容器技术带来了许多便利,例如应用程序部署和分发。它还为日志处理带来了许多挑战,包括:</p><ol><li>如果将日志保存在容器中,则在删除容器时日志将消失。容器的生命周期比虚拟机短得多,因为容器是经常创建和删除的。因此,您需要找到一种永久保存日志的方法。</li><li>在容器时代,要管理的对象比虚拟机和物理机更多。登录到目标容器并解决问题,这使问题变得复杂并增加了成本。</li><li>使用容器技术,可以更轻松地实现微服务。当它与系统分离时,会带来更多的组件。</li></ol><p>我们需要技术来全面了解系统的运行状态,快速发现问题并准确还原上下文。</p><h2 id="Docker-日志管理机制"><a href="#Docker-日志管理机制" class="headerlink" title="Docker 日志管理机制"></a>Docker 日志管理机制</h2><p>Docker 作为 system service 启动,其本身的日志可以通过 <code>sudo journalctl -u docker</code> 命令来查看。容器的日志管理则是通过 Docker 的 logging driver 实现的,获取的是容器标准输出和标准错误输出的内容。获取到了日志信息,当然需要有输出的目的地,所以就有了下面的 12 种日志驱动。</p><img src="https://images.961110.xyz:5001/i/2021/12/07/Docker-.png" style="zoom:50%;" /><h3 id="logging-driver"><a href="#logging-driver" class="headerlink" title="logging driver"></a>logging driver</h3><p>Docker 目前一共支持 12 种日志驱动。</p><table><thead><tr><th>Driver</th><th>Description</th></tr></thead><tbody><tr><td><code>none</code></td><td>不输出任何容器日志,相当于是禁用容器日志。</td></tr><tr><td><a href="https://docs.docker.com/config/containers/logging/local/"><code>local</code></a></td><td>为了最小化记录日志的代价,以自定义的格式存储 log。</td></tr><tr><td><a href="https://docs.docker.com/config/containers/logging/json-file/"><code>json-file</code></a></td><td>Docker 默认的日志驱动,以 JSON 格式保存日志。</td></tr><tr><td><a href="https://docs.docker.com/config/containers/logging/syslog/"><code>syslog</code></a></td><td>Linux 上的日志管理服务,需要 syslog 守护程序 running。</td></tr><tr><td><a href="https://docs.docker.com/config/containers/logging/journald/"><code>journald</code></a></td><td>Linux 上的日志管理服务,需要 journald 守护程序 running。</td></tr><tr><td><a href="https://docs.docker.com/config/containers/logging/gelf/"><code>gelf</code></a></td><td>开源的日志管理方案,以 Graylog Extended Log Format (GELF) 格式输出到例如 Graylog、Logstash 等终端节点。</td></tr><tr><td><a href="https://docs.docker.com/config/containers/logging/fluentd/"><code>fluentd</code></a></td><td>开源的日志管理方案,将日志写入 fluentd,其守护程序必须在主机上运行。</td></tr><tr><td><a href="https://docs.docker.com/config/containers/logging/awslogs/"><code>awslogs</code></a></td><td>第三方日志托管服务,Amazon CloudWatch Logs</td></tr><tr><td><a href="https://docs.docker.com/config/containers/logging/splunk/"><code>splunk</code></a></td><td>第三方日志托管服务,通过HTTP Event Collector将日志写入splunk。</td></tr><tr><td><a href="https://docs.docker.com/config/containers/logging/etwlogs/"><code>etwlogs</code></a></td><td>将日志作为ETW(Event Tracing for Windows)事件写入,只在Windows平台可用。</td></tr><tr><td><a href="https://docs.docker.com/config/containers/logging/gcplogs/"><code>gcplogs</code></a></td><td>第三方日志托管服务,将日志写入 Google Cloud Platform (GCP) Logging</td></tr><tr><td><a href="https://docs.docker.com/config/containers/logging/logentries/"><code>logentries</code></a></td><td>第三方日志托管服务,将日志写入Rapid7 Logentries。</td></tr></tbody></table><p>在 Docker Engine 19.03 以及之前的版本中,<code>docker log</code>这个命令只支持 local、json-file 和 journald 三个 logging driver,Docker 20.10 通过 <strong>dual logging</strong> 实现了该命令对任意 logging driver 的支持。查看详情:<a href="https://docs.docker.com/config/containers/logging/dual-logging/">Use docker logs with remote logging drivers</a></p><h3 id="json-file"><a href="#json-file" class="headerlink" title="json-file"></a>json-file</h3><p>json-file 是 Docker 默认的日志驱动,除非在启动时指定日志驱动(<code>docker run -d --log-driver=syslog ......</code>),可以通过 <code>docker info | grep ""Logging Driver</code> 来确认。</p><p>Docker Root 目录是 <code>/var/lib/docker</code>,所以容器日志对应的目录是 <code>/var/lib/docker/containers/<container-id>/<container-id>-json.log</code></p><p>也可以通过 <code>docker inspect <container-id> | grep "LogPath"</code>来快速查看。</p><p>json-file 将日志的每一行封装到一个 JSON 串中,因此像 Java 的异常栈日志将会被拆分为多条 JSON ,在处理的时候可能需要做合并。</p><h2 id="Docker-原生健康检查能力"><a href="#Docker-原生健康检查能力" class="headerlink" title="Docker 原生健康检查能力"></a>Docker 原生健康检查能力</h2><p>通过在 Dockerfile 中使用<code>HEALTHCHECK</code>指令对容器的运行状态进行检查,可以指定容器初始化时间、检查状态的时间间隔、服务超时时间、重试次数,例:</p><figure class="highlight dockerfile"><table><tr><td class="code"><pre><span class="line"><span class="keyword">HEALTHCHECK</span><span class="bash"> --interval=5m --timeout=3s \</span></span><br><span class="line"><span class="bash"> CMD curl -f http://localhost/ || <span class="built_in">exit</span> 1</span></span><br></pre></td></tr></table></figure><p>每五分钟左右检查一次 web 服务器是否能在三秒内为站点的主页提供服务。</p><p>容器的健康状态都会存储在容器状态文件中。</p><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><ol><li><a href="https://www.codenong.com/db9e4f1d8ec766d4bbc4/">容器日志记录的技术最佳实践:Docker 案例研究</a></li><li><a href="https://segmentfault.com/a/1190000039073943">Docker 容器健康检查</a></li><li><a href="https://www.cnblogs.com/edisonchou/p/docker_logs_study_summary_part1.html">你必须知道的容器日志 (1) Docker logs & logging driver</a></li><li><a href="https://www.codenong.com/db9e4f1d8ec766d4bbc4/">容器日志记录的技术最佳实践:Docker案例研究</a></li><li><a href="https://www.cnblogs.com/operationhome/p/10907591.html">Docker容器日志管理最佳实践</a></li></ol>]]></content>
<categories>
<category> Docker </category>
</categories>
<tags>
<tag> Docker </tag>
<tag> 笔记 </tag>
<tag> 毕设 </tag>
</tags>
</entry>
<entry>
<title>Docker源码分析(十一)—— 命令 docker rm 的执行</title>
<link href="/posts/24743.html"/>
<url>/posts/24743.html</url>
<content type="html"><![CDATA[<blockquote><p>博文分析的 Docker 源码基于 19.03 版本,阅读工具采用 Goland,追踪的是<code>docker rm</code>命令的执行过程。</p><p>第一次阅读 Docker 源码,如有纰漏还请轻喷,喷完希望能够通过邮件或者留言的方式指出问题。</p><p>限于篇幅以及关注重点的原因,部分个人认为不重要的代码会用省略号代替。</p><p>Tips: 如果图片看不清楚可以点击查看大图。</p></blockquote><h2 id="Docker-Client"><a href="#Docker-Client" class="headerlink" title="Docker Client"></a>Docker Client</h2><p>分析的命令越多,越会发现在 Docker Client 这一端中各个命令的实现都基本一样。</p><p>调用链路是 <code>main() --> runDocker() --> newDockerCommand() --> AddCommands() --> cmd.AddCommand() --> container.NewContainerCommand() --> cmd.AddCommand() --> NewRmCommand() --> runRm() --> Client.ContainerRemove()</code></p><p><code>runRm()</code>并行地处理多个容器的 remove 操作,<code>Client.ContainerRemove()</code>向 Daemon 发送 delete 请求:</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line">resp, err := cli.<span class="built_in">delete</span>(ctx, <span class="string">"/containers/"</span>+containerID, query, <span class="literal">nil</span>)</span><br></pre></td></tr></table></figure><p>这一点跟之前分析过的命令不太一样,之前都是 post 请求,<strong>现在成了 delete 请求</strong>。</p><h2 id="Docker-Daemon"><a href="#Docker-Daemon" class="headerlink" title="Docker Daemon"></a>Docker Daemon</h2><p>和 Docker Client 一样,docker rm 对应的路由表项成了</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line">router.NewDeleteRoute(<span class="string">"/containers/{name:.*}"</span>, r.deleteContainers)</span><br></pre></td></tr></table></figure><p>调用 Daemon.ContainerRm() 进一步处理。</p><h3 id="Daemon-ContainerRm"><a href="#Daemon-ContainerRm" class="headerlink" title="Daemon.ContainerRm()"></a>Daemon.ContainerRm()</h3><p>daemon/delete.go: 22</p><img src="https://images.961110.xyz:5001/i/2021/12/03/Daemon.ContainerRmd5e2dae272462ae7.png" style="zoom:50%;" /><p>上面一部分都是一些特殊情况判断,核心函数是 <code>daemon.cleanupContainer()</code></p><h3 id="Daemon-cleanupContainer"><a href="#Daemon-cleanupContainer" class="headerlink" title="Daemon.cleanupContainer()"></a>Daemon.cleanupContainer()</h3><p>daemon/delete.go: 79</p><p><code>Daemon.cleanupContainer()</code>主要是进行容器被 rm 的善后操作。代码比较长,可能先看一下图更好理解一点。</p><img src="https://images.961110.xyz:5001/i/2021/12/03/Daemon.cleanupContainerc4ffc1952d4830a7.png" style="zoom:50%;" /><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(daemon *Daemon)</span> <span class="title">cleanupContainer</span><span class="params">(container *container.Container, forceRemove, removeVolume <span class="keyword">bool</span>)</span> <span class="params">(err error)</span></span> {</span><br><span class="line"></span><br><span class="line"><span class="comment">// 首先判断容器是否正在 running</span></span><br><span class="line"><span class="keyword">if</span> container.IsRunning() {</span><br><span class="line"> <span class="comment">// 正在 running,如果不能强行 remove 的话就需要抛出错误</span></span><br><span class="line"><span class="keyword">if</span> !forceRemove {</span><br><span class="line">state := container.StateString()</span><br><span class="line">procedure := <span class="string">"Stop the container before attempting removal or force remove"</span></span><br><span class="line"><span class="keyword">if</span> state == <span class="string">"paused"</span> {</span><br><span class="line">procedure = <span class="string">"Unpause and then "</span> + strings.ToLower(procedure)</span><br><span class="line">}</span><br><span class="line">err := fmt.Errorf(<span class="string">"You cannot remove a %s container %s. %s"</span>, state, container.ID, procedure)</span><br><span class="line"><span class="keyword">return</span> errdefs.Conflict(err)</span><br><span class="line">}</span><br><span class="line"> <span class="comment">// 可以强制 remove,就先 kill 容器</span></span><br><span class="line"><span class="keyword">if</span> err := daemon.Kill(container); err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">return</span> fmt.Errorf(<span class="string">"Could not kill running container %s, cannot remove - %v"</span>, container.ID, err)</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 判断容器的操作系统是否被 host 所支持</span></span><br><span class="line"><span class="keyword">if</span> !system.IsOSSupported(container.OS) {</span><br><span class="line"><span class="keyword">return</span> fmt.Errorf(<span class="string">"cannot remove %s: %s "</span>, container.ID, system.ErrNotSupportedOperatingSystem)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// stop collection of stats for the container regardless</span></span><br><span class="line"><span class="comment">// if stats are currently getting collected.</span></span><br><span class="line"><span class="comment">// 关闭所有订阅了容器状态的订阅者的 channels,删除对应配置</span></span><br><span class="line">daemon.statsCollector.StopCollection(container)</span><br><span class="line"><span class="comment">// 不是 running 状态,也有可能是 pause 等状态,也需要先 stop</span></span><br><span class="line"><span class="keyword">if</span> err = daemon.containerStop(container, <span class="number">3</span>); err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="comment">// 把容器状态保存到磁盘中,这样容器元数据文件删除前发生错误的话重启 docker 也不会让该容器重新启动起来。</span></span><br><span class="line"><span class="keyword">if</span> err := container.CheckpointTo(daemon.containersReplica); err != <span class="literal">nil</span> && !os.IsNotExist(err) {</span><br><span class="line">logrus.Errorf(<span class="string">"Error saving dying container to disk: %v"</span>, err)</span><br><span class="line">}</span><br><span class="line">container.Unlock()</span><br><span class="line"></span><br><span class="line"><span class="comment">// When container creation fails and `RWLayer` has not been created yet, we</span></span><br><span class="line"><span class="comment">// do not call `ReleaseRWLayer`</span></span><br><span class="line"><span class="keyword">if</span> container.RWLayer != <span class="literal">nil</span> {</span><br><span class="line"> <span class="comment">// 如果容器有读写层,就需要释放</span></span><br><span class="line">err := daemon.imageService.ReleaseLayer(container.RWLayer, container.OS)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line">err = errors.Wrapf(err, <span class="string">"container %s"</span>, container.ID)</span><br><span class="line">container.SetRemovalError(err)</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br><span class="line">container.RWLayer = <span class="literal">nil</span></span><br><span class="line">}</span><br><span class="line"><span class="comment">// 确保容器的 home 目录下所有内容已移除</span></span><br><span class="line"><span class="keyword">if</span> err := system.EnsureRemoveAll(container.Root); err != <span class="literal">nil</span> {</span><br><span class="line">e := errors.Wrapf(err, <span class="string">"unable to remove filesystem for %s"</span>, container.ID)</span><br><span class="line">container.SetRemovalError(e)</span><br><span class="line"><span class="keyword">return</span> e</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">// 删除所有和指定容器相关的容器间的链接</span></span><br><span class="line">linkNames := daemon.linkIndex.<span class="built_in">delete</span>(container)</span><br><span class="line"><span class="comment">// 一些清理操作</span></span><br><span class="line">selinuxFreeLxcContexts(container.ProcessLabel)</span><br><span class="line">daemon.idIndex.Delete(container.ID)</span><br><span class="line">daemon.containers.Delete(container.ID)</span><br><span class="line">daemon.containersReplica.Delete(container)</span><br><span class="line"><span class="comment">// 移除容器的所有挂载点</span></span><br><span class="line"><span class="keyword">if</span> e := daemon.removeMountPoints(container, removeVolume); e != <span class="literal">nil</span> {</span><br><span class="line">logrus.Error(e)</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 释放容器相关的所有别名</span></span><br><span class="line"><span class="keyword">for</span> _, name := <span class="keyword">range</span> linkNames {</span><br><span class="line">daemon.releaseName(name)</span><br><span class="line">}</span><br><span class="line">container.SetRemoved()</span><br><span class="line">stateCtr.del(container.ID)</span><br><span class="line"></span><br><span class="line">daemon.LogContainerEvent(container, <span class="string">"destroy"</span>)</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>daemon.statsCollector.StopCollection(container)</code>这一句中的 <code>statsCollector</code> 的结构体如下</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="keyword">type</span> Collector <span class="keyword">struct</span> {</span><br><span class="line">m sync.Mutex</span><br><span class="line">cond *sync.Cond</span><br><span class="line">supervisor supervisor</span><br><span class="line">interval time.Duration</span><br><span class="line">publishers <span class="keyword">map</span>[*container.Container]*pubsub.Publisher</span><br><span class="line">bufReader *bufio.Reader</span><br><span class="line"></span><br><span class="line"><span class="comment">// The following fields are not set on Windows currently.</span></span><br><span class="line">clockTicksPerSecond <span class="keyword">uint64</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>应该是负责管理和提供容器状态的类,<code>publishers</code>负责向容器状态订阅者推送状态信息。</p><h3 id="daemon-imageService-ReleaseLayer"><a href="#daemon-imageService-ReleaseLayer" class="headerlink" title="daemon.imageService.ReleaseLayer()"></a>daemon.imageService.ReleaseLayer()</h3><p>daemon/images/service.go: 182</p><p>这个函数会在 rm 容器或者 export 容器的时候调用,主要是调用 <code>layerStore.ReleaseRWLayer()</code> 进一步释放读写层。</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(i *ImageService)</span> <span class="title">ReleaseLayer</span><span class="params">(rwlayer layer.RWLayer, containerOS <span class="keyword">string</span>)</span> <span class="title">error</span></span> {</span><br><span class="line"> metadata, err := i.layerStores[containerOS].ReleaseRWLayer(rwlayer)</span><br><span class="line"> layer.LogReleaseMetadata(metadata)</span><br><span class="line"> <span class="keyword">if</span> err != <span class="literal">nil</span> && err != layer.ErrMountDoesNotExist && !os.IsNotExist(errors.Cause(err)) {</span><br><span class="line"> <span class="keyword">return</span> errors.Wrapf(err, <span class="string">"driver %q failed to remove root filesystem"</span>,</span><br><span class="line"> i.layerStores[containerOS].DriverName())</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="layerStore-ReleaseRWLayer"><a href="#layerStore-ReleaseRWLayer" class="headerlink" title="layerStore.ReleaseRWLayer()"></a>layerStore.ReleaseRWLayer()</h3><p>layer/layer_store.go: 587</p><p>这个函数直接看图吧,在看的时候发现还需要多补充下镜像相关的知识。</p><img src="https://images.961110.xyz:5001/i/2021/12/04/layerStore.ReleaseRWLayer.png" style="zoom:50%;" /><h2 id="docker-rm-命令执行过程"><a href="#docker-rm-命令执行过程" class="headerlink" title="docker rm 命令执行过程"></a>docker rm 命令执行过程</h2><p><img src="https://images.961110.xyz:5001/i/2021/12/04/docker-rm-.png"></p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><p>无</p>]]></content>
<categories>
<category> Docker </category>
</categories>
<tags>
<tag> Docker </tag>
<tag> 笔记 </tag>
<tag> 毕设 </tag>
<tag> 源码分析 </tag>
</tags>
</entry>
<entry>
<title>Docker源码分析(十)—— 命令 docker unpause 的执行</title>
<link href="/posts/49360.html"/>
<url>/posts/49360.html</url>
<content type="html"><![CDATA[<blockquote><p>博文分析的 Docker 源码基于 19.03 版本,阅读工具采用 Goland,追踪的是<code>docker unpause</code>命令的执行过程。</p><p>第一次阅读 Docker 源码,如有纰漏还请轻喷,喷完希望能够通过邮件或者留言的方式指出问题。</p><p>限于篇幅以及关注重点的原因,部分个人认为不重要的代码会用省略号代替。</p><p>Tips: 如果图片看不清楚可以点击查看大图。</p></blockquote><h2 id="Docker-Client"><a href="#Docker-Client" class="headerlink" title="Docker Client"></a>Docker Client</h2><p>分析的命令越多,越会发现在 Docker Client 这一端中各个命令的实现都基本一样。</p><p>调用链路是 <code>main() --> runDocker() --> newDockerCommand() --> AddCommands() --> cmd.AddCommand() --> container.NewContainerCommand() --> cmd.AddCommand() --> NewUnpauseCommand() --> runUnpause() --> Client.ContainerUnpause()</code></p><p><code>runUnpause()</code>并行地处理多个容器的 unpause 操作,<code>Client.ContainerUnpause()</code>向 Daemon 发送 unpause 请求:</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line">resp, err := cli.post(ctx, <span class="string">"/containers/"</span>+containerID+<span class="string">"/unpause"</span>, <span class="literal">nil</span>, <span class="literal">nil</span>, <span class="literal">nil</span>)</span><br></pre></td></tr></table></figure><h2 id="Docker-Daemon"><a href="#Docker-Daemon" class="headerlink" title="Docker Daemon"></a>Docker Daemon</h2><p>和前面的几篇一样,直接分析 unpause 路由表项对应的处理函数 <code>containerRouter.postContainersUnpause()</code>,估计和 pause 命令的执行过程正好反过来了。</p><h3 id="containerRouter-postContainersUnpause"><a href="#containerRouter-postContainersUnpause" class="headerlink" title="containerRouter.postContainersUnpause()"></a>containerRouter.postContainersUnpause()</h3><p>api/server/router/container/container_routes.go: 307</p><p>老一套,上图。</p><img src="https://images.961110.xyz:5001/i/2021/12/02/containerRouter.postContainersUnpause.png" style="zoom:50%;" /><p>调用的核心的处理函数 <code>Daemon.containerUnpause()</code></p><h3 id="Daemon-containerUnpause"><a href="#Daemon-containerUnpause" class="headerlink" title="Daemon.containerUnpause()"></a>Daemon.containerUnpause()</h3><p>daemon/unpause.go: 21</p><p>从这个函数的代码来看,比 docker pause 的还要好理解</p><p>直接看图吧</p><img src="https://images.961110.xyz:5001/i/2021/12/02/Daemon.containerUnpause.png" style="zoom:50%;" /><p>其中 <code>daemon.containerd.Resume(context.Background(), container.ID)</code> 这个操作和 <a href="https://blog.961110.xyz/posts/52812.html#client-Pause">Docker源码分析(九)—— 命令 docker pause 的执行</a> 中的流程都是一个意思,等拓展了 containerd 相关的知识后再补充。</p><h2 id="docker-unpause-命令执行过程"><a href="#docker-unpause-命令执行过程" class="headerlink" title="docker unpause 命令执行过程"></a>docker unpause 命令执行过程</h2><img src="https://images.961110.xyz:5001/i/2021/12/02/docker-unpause-.png" style="zoom:50%;" /><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><p>无</p>]]></content>
<categories>
<category> Docker </category>
</categories>
<tags>
<tag> Docker </tag>
<tag> 笔记 </tag>
<tag> 毕设 </tag>
<tag> 源码分析 </tag>
</tags>
</entry>
<entry>
<title>Docker源码分析(九)—— 命令 docker pause 的执行</title>
<link href="/posts/52812.html"/>
<url>/posts/52812.html</url>
<content type="html"><![CDATA[<blockquote><p>博文分析的 Docker 源码基于 19.03 版本,阅读工具采用 Goland,追踪的是<code>docker pause</code>命令的执行过程。</p><p>第一次阅读 Docker 源码,如有纰漏还请轻喷,喷完希望能够通过邮件或者留言的方式指出问题。</p><p>限于篇幅以及关注重点的原因,部分个人认为不重要的代码会用省略号代替。</p><p>Tips: 如果图片看不清楚可以点击查看大图。</p></blockquote><h2 id="Docker-Client"><a href="#Docker-Client" class="headerlink" title="Docker Client"></a>Docker Client</h2><p>废话都不说了,上调用链路:</p><p><code>main() --> runDocker() --> newDockerCommand() --> AddCommands() --> cmd.AddCommand() --> container.NewContainerCommand() --> cmd.AddCommand() --> NewPauseCommand() --> runPause() --> Client.ContainerPause()</code></p><p><code>runPause()</code>并行地处理多个容器的 pause 操作,<code>Client.ContainerPause()</code>向 Daemon 发送 pause 请求:</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line">resp, err := cli.post(ctx, <span class="string">"/containers/"</span>+containerID+<span class="string">"/pause"</span>, <span class="literal">nil</span>, <span class="literal">nil</span>, <span class="literal">nil</span>)</span><br></pre></td></tr></table></figure><h2 id="Docker-Daemon"><a href="#Docker-Daemon" class="headerlink" title="Docker Daemon"></a>Docker Daemon</h2><p>和前面的几篇一样,直接分析 pause 路由表项对应的处理函数 <code>containerRouter.postContainersRestart()</code></p><p>这个函数位于 api/server/router/container/container_routes.go: 293,简单到已经不用上图不用上代码的程度了,作用:</p><ol><li>解析请求的表单</li><li>调用 <code>Daemon.ContainerPause()</code> 进一步处理</li></ol><p> <code>Daemon.ContainerPause()</code>也是同样的简单,作用:</p><ol><li>根据传入参数获取指定容器的结构体对象</li><li>调用 <code>Daemon.containerPause()</code> 进一步处理</li></ol><h3 id="Daemon-containerPause"><a href="#Daemon-containerPause" class="headerlink" title="Daemon.containerPause()"></a>Daemon.containerPause()</h3><p>daemon/pause.go: 22</p><p>这个函数整体来说是比较容易理解的,甚至都不需要做解释。先判断容器不能被 pause 的各种可能的状态,然后再通知 containerd 去 pause 容器,接着更新容器的相关状态,最后将容器状态保存到磁盘中。</p><p><strong>code</strong></p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(daemon *Daemon)</span> <span class="title">containerPause</span><span class="params">(container *container.Container)</span> <span class="title">error</span></span> {</span><br><span class="line">container.Lock()</span><br><span class="line"><span class="keyword">defer</span> container.Unlock()</span><br><span class="line"></span><br><span class="line"><span class="comment">// We cannot Pause the container which is not running</span></span><br><span class="line"><span class="keyword">if</span> !container.Running {</span><br><span class="line"><span class="keyword">return</span> errNotRunning(container.ID)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// We cannot Pause the container which is already paused</span></span><br><span class="line"><span class="keyword">if</span> container.Paused {</span><br><span class="line"><span class="keyword">return</span> errNotPaused(container.ID)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// We cannot Pause the container which is restarting</span></span><br><span class="line"><span class="keyword">if</span> container.Restarting {</span><br><span class="line"><span class="keyword">return</span> errContainerIsRestarting(container.ID)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> err := daemon.containerd.Pause(context.Background(), container.ID); err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">return</span> fmt.Errorf(<span class="string">"Cannot pause container %s: %s"</span>, container.ID, err)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">container.Paused = <span class="literal">true</span></span><br><span class="line">daemon.setStateCounter(container)</span><br><span class="line">daemon.updateHealthMonitor(container)</span><br><span class="line">daemon.LogContainerEvent(container, <span class="string">"pause"</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> err := container.CheckpointTo(daemon.containersReplica); err != <span class="literal">nil</span> {</span><br><span class="line">logrus.WithError(err).Warn(<span class="string">"could not save container to disk"</span>)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>见下图</p><img src="https://images.961110.xyz:5001/i/2021/12/02/Daemon.containerPause.png" style="zoom: 50%;" /><p>如果这个命令分析到这里就结束,那这篇博文看起来挺水的,所以接着 <code>daemon.containerd.Pause()</code> 往下看看 containerd 的执行。</p><h3 id="client-Pause"><a href="#client-Pause" class="headerlink" title="client.Pause()"></a>client.Pause()</h3><p>libcontainerd/remote/client.go: 335</p><p>从调用路径可以看出已经在 libcontainerd 的范围内了,先看看架构图唤醒一下记忆。</p><img src="https://images.961110.xyz:5001/i/2021/11/16/Docker.png" style="zoom: 50%;" /><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *client)</span> <span class="title">Pause</span><span class="params">(ctx context.Context, containerID <span class="keyword">string</span>)</span> <span class="title">error</span></span> {</span><br><span class="line">p, err := c.getProcess(ctx, containerID, libcontainerdtypes.InitProcessName)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> wrapError(p.(containerd.Task).Pause(ctx))</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个函数的代码比较简短,就两个作用</p><ol><li><p>获取指定容器对应的 init 进程。</p><p><code>p, err := c.getProcess(ctx, containerID, libcontainerdtypes.InitProcessName)</code></p><p>这里的 c 是 remote 包下的一个客户端,实现如下:</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="keyword">type</span> client <span class="keyword">struct</span> {</span><br><span class="line"> client *containerd.Client</span><br><span class="line"> stateDir <span class="keyword">string</span></span><br><span class="line"> logger *logrus.Entry</span><br><span class="line"> ns <span class="keyword">string</span></span><br><span class="line"></span><br><span class="line"> backend libcontainerdtypes.Backend</span><br><span class="line"> eventQ queue.Queue</span><br><span class="line"> oomMu sync.Mutex</span><br><span class="line"> oom <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="keyword">bool</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>包含了 containerd 的客户端以及一些配置,<code>libcontainerdtypes.Backend</code>指的是 Docker Daemon。</p><p>process 的结构体实现如下</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="keyword">type</span> process <span class="keyword">struct</span> {</span><br><span class="line">id <span class="keyword">string</span></span><br><span class="line">task *task</span><br><span class="line">pid <span class="keyword">uint32</span></span><br><span class="line">io cio.IO</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>task 的结构体实现如下</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="keyword">type</span> task <span class="keyword">struct</span> {</span><br><span class="line">client *Client</span><br><span class="line"></span><br><span class="line">io cio.IO</span><br><span class="line">id <span class="keyword">string</span></span><br><span class="line">pid <span class="keyword">uint32</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里的 Client 指的是 containerd 与其各种服务交互的客户端</p></li><li><p>开启 Pause 任务 <code>p.(containerd.Task).Pause(ctx)</code></p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(t *task)</span> <span class="title">Pause</span><span class="params">(ctx context.Context)</span> <span class="title">error</span></span> {</span><br><span class="line">_, err := t.client.TaskService().Pause(ctx, &tasks.PauseTaskRequest{</span><br><span class="line">ContainerID: t.id,</span><br><span class="line">})</span><br><span class="line"><span class="keyword">return</span> errdefs.FromGRPC(err)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>t.client.TaskService()</code>这里返回的是底层的 TasksClient,然后发送 pause 请求</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *tasksClient)</span> <span class="title">Pause</span><span class="params">(ctx context.Context, in *PauseTaskRequest, opts ...grpc.CallOption)</span> <span class="params">(*types1.Empty, error)</span></span> {</span><br><span class="line">out := <span class="built_in">new</span>(types1.Empty)</span><br><span class="line">err := c.cc.Invoke(ctx, <span class="string">"/containerd.services.tasks.v1.Tasks/Pause"</span>, in, out, opts...)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> out, <span class="literal">nil</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure></li></ol><p>在看 containerd 的代码的时候,因为不太了解其工作原理,所以看的时候比较难理解,深入不下去了,等了解了之后再往下看。</p><h2 id="docker-pause-命令的执行过程"><a href="#docker-pause-命令的执行过程" class="headerlink" title="docker pause 命令的执行过程"></a>docker pause 命令的执行过程</h2><img src="https://images.961110.xyz:5001/i/2021/12/02/docker-pause-2bba1dc72b343bec.png" style="zoom: 40%;" /><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><p>无</p>]]></content>
<categories>
<category> Docker </category>
</categories>
<tags>
<tag> Docker </tag>
<tag> 笔记 </tag>
<tag> 毕设 </tag>
<tag> 源码分析 </tag>
</tags>
</entry>
<entry>
<title>Docker源码分析(八)—— 命令 docker restart 的执行</title>
<link href="/posts/20332.html"/>
<url>/posts/20332.html</url>
<content type="html"><![CDATA[<blockquote><p>博文分析的 Docker 源码基于 19.03 版本,阅读工具采用 Goland,追踪的是<code>docker restart</code>命令的执行过程。</p><p>第一次阅读 Docker 源码,如有纰漏还请轻喷,喷完希望能够通过邮件或者留言的方式指出问题。</p><p>限于篇幅以及关注重点的原因,部分个人认为不重要的代码会用省略号代替。</p><p>Tips: 如果图片看不清楚可以点击查看大图。</p></blockquote><h2 id="Docker-Client"><a href="#Docker-Client" class="headerlink" title="Docker Client"></a>Docker Client</h2><p>分析的命令越多,越会发现在 Docker Client 这一端中各个命令的实现都基本一样。</p><p>调用链路是 <code>main() --> runDocker() --> newDockerCommand() --> AddCommands() --> cmd.AddCommand() --> container.NewContainerCommand() --> cmd.AddCommand() --> NewRestartCommand() --> runRestart() --> Client.ContainerRestart()</code></p><p>因为 <a href="https://blog.961110.xyz/posts/40974.html#Docker-Client">前一篇</a> 中已经分析过了 docker stop 命令的执行函数<code>runStop()</code>,<code>runRestart()</code>与其一样都是并行地处理多个容器的 restart 操作。</p><h3 id="Client-ContainerRestart"><a href="#Client-ContainerRestart" class="headerlink" title="Client.ContainerRestart()"></a>Client.ContainerRestart()</h3><p>github.com/docker/docker/client/container_restart.go: 14</p><p><code>Client.ContainerRestart()</code>的实现非常简单,作用也非常简单,就是向 Daemon 发送 restart 请求。</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="comment">// ContainerRestart stops and starts a container again.</span></span><br><span class="line"><span class="comment">// It makes the daemon to wait for the container to be up again for</span></span><br><span class="line"><span class="comment">// a specific amount of time, given the timeout.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(cli *Client)</span> <span class="title">ContainerRestart</span><span class="params">(ctx context.Context, containerID <span class="keyword">string</span>, timeout *time.Duration)</span> <span class="title">error</span></span> {</span><br><span class="line">query := url.Values{}</span><br><span class="line"><span class="keyword">if</span> timeout != <span class="literal">nil</span> {</span><br><span class="line">query.Set(<span class="string">"t"</span>, timetypes.DurationToSecondsString(*timeout))</span><br><span class="line">}</span><br><span class="line">resp, err := cli.post(ctx, <span class="string">"/containers/"</span>+containerID+<span class="string">"/restart"</span>, query, <span class="literal">nil</span>, <span class="literal">nil</span>)</span><br><span class="line">ensureReaderClosed(resp)</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到函数这个函数就是向 Daemon 发送了一个 post 请求。</p><h2 id="Docker-Daemon"><a href="#Docker-Daemon" class="headerlink" title="Docker Daemon"></a>Docker Daemon</h2><p>和前面的几篇一样,直接分析 restart 路由表项对应的处理函数 <code>containerRouter.postContainersRestart()</code></p><h3 id="containerRouter-postContainersRestart"><a href="#containerRouter-postContainersRestart" class="headerlink" title="containerRouter.postContainersRestart()"></a>containerRouter.postContainersRestart()</h3><p>api/server/router/container/container_routes.go: 235</p><p>还是那老一套,直接上图,清晰明了。</p><table> <tr> <td> <center><img src="https://images.961110.xyz:5001/i/2021/12/01/containerRouter.postContainersRestart5c00ef290227e48e.png">图1 containerRouter.postContainersRestart() </center> </td> <td> <center><img src="https://images.961110.xyz:5001/i/2021/12/01/Daemon.ContainerRestart.png">图2 Daemon.ContainerRestart()</center> </td> </tr></table><p>这两个函数都是比较简单的,做了一点点操作,调用了核心的处理函数 <code>Daemon.containerRestart()</code></p><h3 id="Daemon-containerRestart"><a href="#Daemon-containerRestart" class="headerlink" title="Daemon.containerRestart()"></a>Daemon.containerRestart()</h3><p>daemon/restart.go: 37</p><p>这个函数整体来说是比较容易理解的,就是先 stop 然后再 start,但是<strong>有一点不理解的就是为什么当容器隔离方式不是 HyperV 时,需要再一次挂载容器的 BaseFS,然后在<code>containerRestart()</code>结束后再取消挂载。</strong>这个函数也基本都是调用之前分析过的函数,空降导航</p><ol><li><a href="https://blog.961110.xyz/posts/56133.html#Docker-Daemon-%E5%A4%84%E7%90%86%E5%90%AF%E5%8A%A8%E5%AE%B9%E5%99%A8%E7%9A%84%E8%AF%B7%E6%B1%82%E6%B5%81%E7%A8%8B">Docker源码分析(三)—— docker start 命令在 Docker Daemon中的执行</a></li><li><a href="https://blog.961110.xyz/posts/40974.html#Docker-Daemon">Docker源码分析(六)—— 命令 docker stop 的执行</a></li></ol><p>-</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(daemon *Daemon)</span> <span class="title">containerRestart</span><span class="params">(container *container.Container, seconds <span class="keyword">int</span>)</span> <span class="title">error</span></span> {</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Determine isolation. If not specified in the hostconfig, use daemon default.</span></span><br><span class="line"> actualIsolation := container.HostConfig.Isolation</span><br><span class="line"> <span class="keyword">if</span> containertypes.Isolation.IsDefault(actualIsolation) {</span><br><span class="line"> actualIsolation = daemon.defaultIsolation</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Avoid unnecessarily unmounting and then directly mounting</span></span><br><span class="line"> <span class="comment">// the container when the container stops and then starts</span></span><br><span class="line"> <span class="comment">// again. We do not do this for Hyper-V isolated containers</span></span><br><span class="line"> <span class="comment">// (implying also on Windows) as the HCS must have exclusive</span></span><br><span class="line"> <span class="comment">// access to mount the containers filesystem inside the utility</span></span><br><span class="line"> <span class="comment">// VM.</span></span><br><span class="line"> <span class="keyword">if</span> !containertypes.Isolation.IsHyperV(actualIsolation) {</span><br><span class="line"> <span class="keyword">if</span> err := daemon.Mount(container); err == <span class="literal">nil</span> {</span><br><span class="line"> <span class="keyword">defer</span> daemon.Unmount(container)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> container.IsRunning() {</span><br><span class="line"> <span class="comment">// 将容器的 autoremove 设置为 false,防止被自动删除</span></span><br><span class="line"> autoRemove := container.HostConfig.AutoRemove</span><br><span class="line"></span><br><span class="line"> container.HostConfig.AutoRemove = <span class="literal">false</span></span><br><span class="line"> err := daemon.containerStop(container, seconds)</span><br><span class="line"> <span class="comment">// 不管有没有 stop,都需要恢复原来的设置,并且因为容器在 stop 的时候会将 HostConfig 保存到磁盘中,所以恢复了 autoremove 设置之后也需要保存到磁盘中</span></span><br><span class="line"> container.HostConfig.AutoRemove = autoRemove</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> toDiskErr := daemon.checkpointAndSave(container); toDiskErr != <span class="literal">nil</span> {</span><br><span class="line"> logrus.Errorf(<span class="string">"Write container to disk error: %v"</span>, toDiskErr)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 启动</span></span><br><span class="line"> <span class="keyword">if</span> err := daemon.containerStart(container, <span class="string">""</span>, <span class="string">""</span>, <span class="literal">true</span>); err != <span class="literal">nil</span> {</span><br><span class="line"> <span class="keyword">return</span> err</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> daemon.LogContainerEvent(container, <span class="string">"restart"</span>)</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><img src="https://images.961110.xyz:5001/i/2021/12/02/Daemon.ContainerRestart.png" style="zoom:33%;" /><h2 id="docker-restart-命令执行过程"><a href="#docker-restart-命令执行过程" class="headerlink" title="docker restart 命令执行过程"></a>docker restart 命令执行过程</h2><img src="https://images.961110.xyz:5001/i/2021/12/02/docker-restart-.png" style="zoom:33%;" /><p>这里放上之前分析过的两个函数</p><table> <tr> <td> <center><img src="https://images.961110.xyz:5001/i/2021/12/02/daemon.killWithSignal.png">图1 daemon.killWithSignal()</center> </td> <td> <center><img src="https://images.961110.xyz:5001/i/2021/11/19/containerStart.png">图2 containerStart()</center> </td> </tr></table><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><p>无</p>]]></content>
<categories>
<category> Docker </category>
</categories>
<tags>
<tag> Docker </tag>
<tag> 笔记 </tag>
<tag> 毕设 </tag>
<tag> 源码分析 </tag>
</tags>
</entry>
<entry>
<title>Docker源码分析(七)—— 命令 docker kill 的执行</title>
<link href="/posts/37106.html"/>
<url>/posts/37106.html</url>
<content type="html"><![CDATA[<blockquote><p>博文分析的 Docker 源码基于 19.03 版本,阅读工具采用 Goland,追踪的是<code>docker kill</code>命令的执行过程。</p><p>第一次阅读 Docker 源码,如有纰漏还请轻喷,喷完希望能够通过邮件或者留言的方式指出问题。</p><p>限于篇幅以及关注重点的原因,部分个人认为不重要的代码会用省略号代替。</p><p>Tips: 如果图片看不清楚可以点击查看大图。</p></blockquote><h2 id="Docker-Client"><a href="#Docker-Client" class="headerlink" title="Docker Client"></a>Docker Client</h2><p>分析的命令越多,越会发现在 Docker Client 这一端中各个命令的实现都基本一样。</p><p>调用链路是 <code>main() --> runDocker() --> newDockerCommand() --> AddCommands() --> cmd.AddCommand() --> container.NewContainerCommand() --> cmd.AddCommand() --> NewKillCommand() --> runKill() --> Client.ContainerKill()</code></p><p>因为 <a href="https://blog.961110.xyz/posts/40974.html#Docker-Client">前一篇</a> 中已经分析过了 docker stop 命令的执行函数<code>runStop()</code>,<code>runKill()</code>与其一样都是并行地处理多个容器的 kill 操作。</p><h3 id="Client-ContainerKill"><a href="#Client-ContainerKill" class="headerlink" title="Client.ContainerKill()"></a>Client.ContainerKill()</h3><p>github.com/docker/docker/client/container_kill.go: 9</p><p><code>Client.ContainerKill()</code>的实现非常简单,作用也非常简单,就是终结指定容器的进程。</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="comment">// ContainerKill terminates the container process but does not remove the container from the docker host.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(cli *Client)</span> <span class="title">ContainerKill</span><span class="params">(ctx context.Context, containerID, signal <span class="keyword">string</span>)</span> <span class="title">error</span></span> {</span><br><span class="line">query := url.Values{}</span><br><span class="line">query.Set(<span class="string">"signal"</span>, signal)</span><br><span class="line"></span><br><span class="line">resp, err := cli.post(ctx, <span class="string">"/containers/"</span>+containerID+<span class="string">"/kill"</span>, query, <span class="literal">nil</span>, <span class="literal">nil</span>)</span><br><span class="line">ensureReaderClosed(resp)</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到函数这个函数就是向 Daemon 发送了一个 post 请求。</p><h2 id="Docker-Daemon"><a href="#Docker-Daemon" class="headerlink" title="Docker Daemon"></a>Docker Daemon</h2><p>有了之前的经验,同样从 Daemon 的路由入手。处理过程中一些前置的函数已经在前面的博文中分析过,这里直接分析路由处理函数<code>containerRouter.postContainersKill()</code></p><h3 id="containerRouter-postContainersKill"><a href="#containerRouter-postContainersKill" class="headerlink" title="containerRouter.postContainersKill()"></a>containerRouter.postContainersKill()</h3><p>api/server/router/container/container_routes.go: 235</p><p>函数的功能比较简单,看图就一目了然。</p><img src="https://images.961110.xyz:5001/i/2021/12/01/postContainerKill.png" style="zoom:50%;" /><p>对 <code>Daemon.ContainerKill()</code> 进行进一步分析。</p><h3 id="Daemon-ContainerKill"><a href="#Daemon-ContainerKill" class="headerlink" title="Daemon.ContainerKill()"></a>Daemon.ContainerKill()</h3><p>daemon/kill.go: 40</p><p><img src="https://images.961110.xyz:5001/i/2021/12/01/Daemon.ContainerKill4866a473f563b750.png"></p><p>这个函数主要是找到指定的 container 结构体对象,然后检验参数的合法性并根据传入的信号分别进行 kill 操作。</p><p>利用传入的信号进行 kill 操作 <code>daemon.killWithSignal()</code> 这个函数已经在上一篇 <a href="">Docker源码分析(六)—— 命令 docker stop 的执行</a> 分析过了,所以着重看一下 <code>daemon.Kill()</code> 这个函数。</p><h3 id="daemon-Kill"><a href="#daemon-Kill" class="headerlink" title="daemon.Kill()"></a>daemon.Kill()</h3><p>daemon/kill.go: 121</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Kill forcefully terminates a container.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(daemon *Daemon)</span> <span class="title">Kill</span><span class="params">(container *containerpkg.Container)</span> <span class="title">error</span></span> {</span><br><span class="line"><span class="comment">// 判断容器是否在运行</span></span><br><span class="line"><span class="keyword">if</span> !container.IsRunning() {</span><br><span class="line"><span class="keyword">return</span> errNotRunning(container.ID)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 1. Send SIGKILL</span></span><br><span class="line"><span class="comment">// 与 stop 不同的是,这发送的系统调用的 SIGKILL 信号</span></span><br><span class="line"><span class="keyword">if</span> err := daemon.killPossiblyDeadProcess(container, <span class="keyword">int</span>(syscall.SIGKILL)); err != <span class="literal">nil</span> {</span><br><span class="line"><span class="comment">// 如果这里无法 stop 容器,说明其可能已经 stop 了</span></span><br><span class="line"> <span class="comment">// 另外,由于 err 是特定于环境的,无法判断其已经 stop 或者出了其他状况</span></span><br><span class="line"> <span class="comment">// 所以这里等待了两秒再次判断其是否 running,如果其仍在运行,那么就返回遇到的 err</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// 判断是否未找到该进程</span></span><br><span class="line"><span class="keyword">if</span> isErrNoSuchProcess(err) {</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 延迟两秒,判断是否为 running 状态</span></span><br><span class="line">ctx, cancel := context.WithTimeout(context.Background(), <span class="number">2</span>*time.Second)</span><br><span class="line"><span class="keyword">defer</span> cancel()</span><br><span class="line"><span class="comment">// 如果还是超时,那就说明可能发生了一些错误,返回给调用者参考</span></span><br><span class="line"><span class="keyword">if</span> status := <-container.Wait(ctx, containerpkg.WaitConditionNotRunning); status.Err() != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 2. Wait for the process to die, in last resort, try to kill the process directly</span></span><br><span class="line"><span class="comment">// 直接 kill 掉</span></span><br><span class="line"><span class="keyword">if</span> err := killProcessDirectly(container); err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">if</span> isErrNoSuchProcess(err) {</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// Wait for exit with no timeout.</span></span><br><span class="line"><span class="comment">// Ignore returned status.</span></span><br><span class="line"><-container.Wait(context.Background(), containerpkg.WaitConditionNotRunning)</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这部分的代码还是有一点点复杂的,<code>daemon.killPossiblyDeadProcess()</code>这个函数在 <a href="https://blog.961110.xyz/posts/40974.html#Daemon-killWithSignal">上一篇博文</a> 中已经分析过。<code>killProcessDirectly()</code>是 kill 容器的最后杀手锏。</p><p><img src="https://images.961110.xyz:5001/i/2021/12/01/SIGKILL--kill-48c8237b7a5f83ca.png"></p><h3 id="killProcessDirectly"><a href="#killProcessDirectly" class="headerlink" title="killProcessDirectly()"></a>killProcessDirectly()</h3><p>daemon/container_operations_unix.go: 339</p><p>在这个函数中还在苦苦等待容器退出,超时时间是 10 秒,否则就直接调用系统函数 kill 掉。</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">killProcessDirectly</span><span class="params">(cntr *container.Container)</span> <span class="title">error</span></span> {</span><br><span class="line">ctx, cancel := context.WithTimeout(context.Background(), <span class="number">10</span>*time.Second)</span><br><span class="line"><span class="keyword">defer</span> cancel()</span><br><span class="line"></span><br><span class="line"><span class="comment">// 一直阻塞等待容器退出或者超时</span></span><br><span class="line">status := <-cntr.Wait(ctx, container.WaitConditionNotRunning)</span><br><span class="line"><span class="keyword">if</span> status.Err() != <span class="literal">nil</span> {</span><br><span class="line"><span class="comment">// Ensure that we don't kill ourselves</span></span><br><span class="line"><span class="keyword">if</span> pid := cntr.GetPID(); pid != <span class="number">0</span> {</span><br><span class="line">logrus.Infof(<span class="string">"Container %s failed to exit within 10 seconds of kill - trying direct SIGKILL"</span>, stringid.TruncateID(cntr.ID))</span><br><span class="line"><span class="keyword">if</span> err := unix.Kill(pid, <span class="number">9</span>); err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">if</span> err != unix.ESRCH {</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br><span class="line">e := errNoSuchProcess{pid, <span class="number">9</span>}</span><br><span class="line">logrus.Debug(e)</span><br><span class="line"><span class="keyword">return</span> e</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="docker-kill-执行流程分析"><a href="#docker-kill-执行流程分析" class="headerlink" title="docker kill 执行流程分析"></a>docker kill 执行流程分析</h2><img src="https://images.961110.xyz:5001/i/2021/12/01/docker-kill-b3f69031d11d45c8.png" style="zoom: 25%;" /><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><p>无</p>]]></content>
<categories>
<category> Docker </category>
</categories>
<tags>
<tag> Docker </tag>
<tag> 笔记 </tag>
<tag> 毕设 </tag>
<tag> 源码分析 </tag>
</tags>
</entry>
<entry>
<title>Docker源码分析(六)—— 命令 docker stop 的执行</title>
<link href="/posts/40974.html"/>
<url>/posts/40974.html</url>
<content type="html"><![CDATA[<blockquote><p>博文分析的 Docker 源码基于 19.03 版本,阅读工具采用 Goland,追踪的是<code>docker stop</code>命令的执行过程。</p><p>第一次阅读 Docker 源码,如有纰漏还请轻喷,喷完希望能够通过邮件或者留言的方式指出问题。</p><p>限于篇幅以及关注重点的原因,部分个人认为不重要的代码会用省略号代替。</p><p>Tips: 如果图片看不清楚可以点击查看大图。</p></blockquote><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>前面的博文已经分析了容器的创建和启动过程,秉持着有始有终的信条,所以本篇博文分析 <code>docker stop</code> 命令的执行过程。</p><p>分析的方式同样和前面几篇一样,从 Docker Client 开始,到 Docker Daemon 结束。</p><h2 id="Docker-Client"><a href="#Docker-Client" class="headerlink" title="Docker Client"></a>Docker Client</h2><p>调用链路是 <code>main() --> runDocker() --> newDockerCommand() --> AddCommands() --> cmd.AddCommand() --> container.NewContainerCommand() --> cmd.AddCommand() --> NewStopCommand() --> runStop()</code></p><p>因为 <a href="https://blog.961110.xyz/posts/21441.html">前一篇</a> 中已经分析过了 cobra 新增命令的函数,所以此处略过。从命令要执行的函数<code>runStop()</code>开始进行分析。</p><h3 id="DockerCli-runStop"><a href="#DockerCli-runStop" class="headerlink" title="DockerCli.runStop()"></a>DockerCli.runStop()</h3><p>cli/command/container/stop.go: 42</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">runStop</span><span class="params">(dockerCli command.Cli, opts *stopOptions)</span> <span class="title">error</span></span> {</span><br><span class="line">ctx := context.Background()</span><br><span class="line"></span><br><span class="line"><span class="comment">// 判断超时时间是否有变化,有就更新为用户设置的超时时间</span></span><br><span class="line"><span class="keyword">var</span> timeout *time.Duration</span><br><span class="line"><span class="keyword">if</span> opts.timeChanged {</span><br><span class="line">timeoutValue := time.Duration(opts.time) * time.Second</span><br><span class="line">timeout = &timeoutValue</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> errs []<span class="keyword">string</span></span><br><span class="line"><span class="comment">// 通过协程并行地 stop 多个容器(也可能是一个)</span></span><br><span class="line">errChan := parallelOperation(ctx, opts.containers, <span class="function"><span class="keyword">func</span><span class="params">(ctx context.Context, id <span class="keyword">string</span>)</span> <span class="title">error</span></span> {</span><br><span class="line"><span class="keyword">return</span> dockerCli.Client().ContainerStop(ctx, id, timeout)</span><br><span class="line">})</span><br><span class="line"></span><br><span class="line"><span class="comment">// 输出 stop 时返回的错误信息</span></span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>根据上面的代码,可以看出需要关注的是 <code>dockerCli.Client().ContainerStop(ctx, id, timeout)</code>,接着往下追溯。</p><p>这里的 <code>dockerCli.Client()</code> 指的是 <code>API Client</code>,在前一篇也解释过,其作用是与 docker server 进行交互,保存着服务端的地址、通信方式(unix. fd, tcp)等信息。</p><h3 id="Client-ContainerStop"><a href="#Client-ContainerStop" class="headerlink" title="Client.ContainerStop()"></a>Client.ContainerStop()</h3><p>github.com/docker/docker/client/container_stop.go: 18</p><p>和前一篇分析过的 <code>Client.ContainerInspectWithRaw()</code> 函数一样,这里执行的代码引用自 Docker Daemon 的实现。</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="comment">// ContainerStop stops a container. In case the container fails to stop</span></span><br><span class="line"><span class="comment">// gracefully within a time frame specified by the timeout argument,</span></span><br><span class="line"><span class="comment">// it is forcefully terminated (killed).</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">// If the timeout is nil, the container's StopTimeout value is used, if set,</span></span><br><span class="line"><span class="comment">// otherwise the engine default. A negative timeout value can be specified,</span></span><br><span class="line"><span class="comment">// meaning no timeout, i.e. no forceful termination is performed.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(cli *Client)</span> <span class="title">ContainerStop</span><span class="params">(ctx context.Context, containerID <span class="keyword">string</span>, timeout *time.Duration)</span> <span class="title">error</span></span> {</span><br><span class="line">query := url.Values{}</span><br><span class="line"><span class="keyword">if</span> timeout != <span class="literal">nil</span> {</span><br><span class="line">query.Set(<span class="string">"t"</span>, timetypes.DurationToSecondsString(*timeout))</span><br><span class="line">}</span><br><span class="line">resp, err := cli.post(ctx, <span class="string">"/containers/"</span>+containerID+<span class="string">"/stop"</span>, query, <span class="literal">nil</span>, <span class="literal">nil</span>)</span><br><span class="line">ensureReaderClosed(resp)</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>根据函数注释,可以看出刚刚提到的超时时间并不是命令执行失败的超时时间,而是容器的 <strong>“死期”</strong>(超时会被无情 kill)。</p><p>这里 API Client 向 Docker Daemon 发送了 stop container 的请求,下面转到 Docker Daemon 端。</p><h3 id="docker-stop-命令在-Docker-Client-中的执行过程"><a href="#docker-stop-命令在-Docker-Client-中的执行过程" class="headerlink" title="docker stop 命令在 Docker Client 中的执行过程"></a>docker stop 命令在 Docker Client 中的执行过程</h3><img src="https://images.961110.xyz:5001/i/2021/11/30/docker-stop--Docker-Client-.png" style="zoom: 50%;" /><p>因为略过了一些之前分析过的函数以及一些自认为不重要的代码,所以感觉这个命令在 Docker Client 端的执行还是非常简单的。</p><h2 id="Docker-Daemon"><a href="#Docker-Daemon" class="headerlink" title="Docker Daemon"></a>Docker Daemon</h2><p>从前几篇的分析总结出经验,命令在 Daemon 端的处理会有对应的路由,所以还是从<code>containerRouter.initRoutes()</code>开始分析(Docker Daemon 其他的分析可以在 <a href="https://blog.961110.xyz/posts/19054.html">之前的文章</a> 中查阅)。</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(r *containerRouter)</span> <span class="title">initRoutes</span><span class="params">()</span></span> {</span><br><span class="line">r.routes = []router.Route{</span><br><span class="line">...</span><br><span class="line">router.NewPostRoute(<span class="string">"/containers/{name:.*}/stop"</span>, r.postContainersStop),</span><br><span class="line">...</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>r.postContainersStop()</code>调用的是<code>containerRouter.backend.ContainerStop()</code>,如果容器不是 running 状态,那就返回错误,否则就调用 <code>daemon.containerStop()</code> 进行进一步处理。</p><h3 id="Daemon-containerStop"><a href="#Daemon-containerStop" class="headerlink" title="Daemon.containerStop()"></a>Daemon.containerStop()</h3><p>daemon/stop.go: 40</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="comment">// containerStop sends a stop signal, waits, sends a kill signal.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(daemon *Daemon)</span> <span class="title">containerStop</span><span class="params">(container *containerpkg.Container, seconds <span class="keyword">int</span>)</span> <span class="title">error</span></span> {</span><br><span class="line">...</span><br><span class="line"><span class="comment">// 获取默认 stop signal 或者容器配置中指定的 stop signal</span></span><br><span class="line">stopSignal := container.StopSignal()</span><br><span class="line"><span class="comment">// 1. Send a stop signal</span></span><br><span class="line"><span class="keyword">if</span> err := daemon.killPossiblyDeadProcess(container, stopSignal); err != <span class="literal">nil</span> {</span><br><span class="line"><span class="comment">//</span></span><br><span class="line">ctx, cancel := context.WithTimeout(context.Background(), <span class="number">2</span>*time.Second)</span><br><span class="line"><span class="keyword">defer</span> cancel()</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> status := <-container.Wait(ctx, containerpkg.WaitConditionNotRunning); status.Err() != <span class="literal">nil</span> {</span><br><span class="line">logrus.Infof(<span class="string">"Container failed to stop after sending signal %d to the process, force killing"</span>, stopSignal)</span><br><span class="line"><span class="keyword">if</span> err := daemon.killPossiblyDeadProcess(container, <span class="number">9</span>); err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 2. Wait for the process to exit on its own</span></span><br><span class="line">ctx := context.Background()</span><br><span class="line"><span class="keyword">if</span> seconds >= <span class="number">0</span> {</span><br><span class="line"><span class="keyword">var</span> cancel context.CancelFunc</span><br><span class="line">ctx, cancel = context.WithTimeout(ctx, time.Duration(seconds)*time.Second)</span><br><span class="line"><span class="keyword">defer</span> cancel()</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> status := <-container.Wait(ctx, containerpkg.WaitConditionNotRunning); status.Err() != <span class="literal">nil</span> {</span><br><span class="line">logrus.Infof(<span class="string">"Container %v failed to exit within %d seconds of signal %d - using the force"</span>, container.ID, seconds, stopSignal)</span><br><span class="line"><span class="comment">// 3. If it doesn't, then send SIGKILL</span></span><br><span class="line"><span class="keyword">if</span> err := daemon.Kill(container); err != <span class="literal">nil</span> {</span><br><span class="line"><span class="comment">// Wait without a timeout, ignore result.</span></span><br><span class="line"><-container.Wait(context.Background(), containerpkg.WaitConditionNotRunning)</span><br><span class="line">logrus.Warn(err) <span class="comment">// Don't return error because we only care that container is stopped, not what function stopped it</span></span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">daemon.LogContainerEvent(container, <span class="string">"stop"</span>)</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>daemon.killPossiblyDeadProcess(container, stopSignal)</code>这个函数实际上是给<code>daemon.killWithSignal(container, sig)</code>传入了<code>stopSignal</code>,所以往下分析一下<code>killWithSignal()</code>。</p><h3 id="Daemon-killWithSignal"><a href="#Daemon-killWithSignal" class="headerlink" title="Daemon.killWithSignal()"></a>Daemon.killWithSignal()</h3><p>daemon/kill.go: 62</p><p>函数的作用就是给指定容器发送指定信号,当然需要判断容器当前可能的各种状态。</p><p>源代码如下:</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(daemon *Daemon)</span> <span class="title">killWithSignal</span><span class="params">(container *containerpkg.Container, sig <span class="keyword">int</span>)</span> <span class="title">error</span></span> {</span><br><span class="line"><span class="comment">// 再次判断是否为 running 状态</span></span><br><span class="line"><span class="keyword">if</span> !container.Running {</span><br><span class="line"><span class="keyword">return</span> errNotRunning(container.ID)</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 判断容器配置中是否自定义了 stop 信号,并且传入的信号不是系统信号中的 KILL 信号</span></span><br><span class="line"><span class="keyword">var</span> unpause <span class="keyword">bool</span></span><br><span class="line"><span class="keyword">if</span> container.Config.StopSignal != <span class="string">""</span> && syscall.Signal(sig) != syscall.SIGKILL {</span><br><span class="line"> <span class="comment">// 解析容器配置中自定义的 stop 信号</span></span><br><span class="line">containerStopSignal, err := signal.ParseSignal(container.Config.StopSignal)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">return</span> err</span><br><span class="line">}</span><br><span class="line"> <span class="comment">// 如果传入的信号就是容器配置中自定义的 stop 信号,就进行相应的操作</span></span><br><span class="line"><span class="keyword">if</span> containerStopSignal == syscall.Signal(sig) {</span><br><span class="line">container.ExitOnNext()</span><br><span class="line">unpause = container.Paused</span><br><span class="line">}</span><br><span class="line">} <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 通知 container.RestartManager() 在容器 stop 后不要重启该容器</span></span><br><span class="line">container.ExitOnNext()</span><br><span class="line">unpause = container.Paused</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 如果 Docker Daemon 不是在 shut down,说明就是手动 stop 的容器</span></span><br><span class="line"><span class="comment">// 于是改变容器的状态,并将状态保存到文件中</span></span><br><span class="line"><span class="keyword">if</span> !daemon.IsShuttingDown() {</span><br><span class="line">container.HasBeenManuallyStopped = <span class="literal">true</span></span><br><span class="line">container.CheckpointTo(daemon.containersReplica)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 如果容器正在重启,因为上面已经告诉 estartManager 在容器 stop 后不要重启该容器</span></span><br><span class="line"><span class="comment">// 所以不需要做额外的操作</span></span><br><span class="line"><span class="keyword">if</span> container.Restarting {</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 发送 stop 信号</span></span><br><span class="line"><span class="keyword">if</span> err := daemon.kill(container, sig); err != <span class="literal">nil</span> {</span><br><span class="line">...</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 如果容器被阻塞,则先恢复,再发送 stop 信号</span></span><br><span class="line"><span class="keyword">if</span> unpause {</span><br><span class="line"><span class="comment">// above kill signal will be sent once resume is finished</span></span><br><span class="line"><span class="keyword">if</span> err := daemon.containerd.Resume(context.Background(), container.ID); err != <span class="literal">nil</span> {</span><br><span class="line">logrus.Warnf(<span class="string">"Cannot unpause container %s: %s"</span>, container.ID, err)</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">...</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这部分代码的执行流程图如下</p><p><img src="https://images.961110.xyz:5001/i/2021/12/02/daemon.killWithSignal.png"></p><h3 id="Daemon-Kill-container"><a href="#Daemon-Kill-container" class="headerlink" title="Daemon.Kill(container)"></a>Daemon.Kill(container)</h3><p>当到了指定 container 的<strong>“死期”</strong>后,会被无情 Kill 掉,这一部分代码的执行流程会在后续对 <code>docker kill</code> 命令的分析中给出。</p><h3 id="docker-stop-命令在-Docker-Daemon-中的执行过程"><a href="#docker-stop-命令在-Docker-Daemon-中的执行过程" class="headerlink" title="docker stop 命令在 Docker Daemon 中的执行过程"></a>docker stop 命令在 Docker Daemon 中的执行过程</h3><img src="https://images.961110.xyz:5001/i/2021/12/01/docker-stop--Docker-Daemon-.png" style="zoom: 25%;" /><p>刚刚说 docker stop 命令在 Docker Client 端执行的过程比较简单,从上图可以看到这条命令的执行重任基本都交给了 Docker Daemon,不过也合情合理,毕竟 Docker Client 就是一个无情的发送命令的机器。</p><h2 id="docker-stop-的执行过程分析"><a href="#docker-stop-的执行过程分析" class="headerlink" title="docker stop 的执行过程分析"></a>docker stop 的执行过程分析</h2><img src="https://images.961110.xyz:5001/i/2021/12/01/docker-stop-d80c82a9612eb64e.png" style="zoom:25%;" /><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><p>无</p>]]></content>
<categories>
<category> Docker </category>
</categories>
<tags>
<tag> Docker </tag>
<tag> 笔记 </tag>
<tag> 毕设 </tag>
<tag> 源码分析 </tag>
</tags>
</entry>
<entry>
<title>Docker 源码梳理文章索引暨容器生命周期相关源码梳理</title>
<link href="/posts/52319.html"/>
<url>/posts/52319.html</url>
<content type="html"><![CDATA[<blockquote><p>持续更新中。。。</p></blockquote><h2 id="源码分析"><a href="#源码分析" class="headerlink" title="源码分析"></a>源码分析</h2><ol><li><a href="https://blog.961110.xyz/posts/64829.html">Docker源码分析(一)—— docker run 命令在 Docker Client 中的执行</a></li><li><a href="https://blog.961110.xyz/posts/19054.html">Docker源码分析(二)—— docker run 命令在 Docker Daemon中的执行</a></li><li><a href="https://blog.961110.xyz/posts/56133.html">Docker源码分析(三)—— docker start 命令在 Docker Daemon中的执行</a></li><li><a href="https://blog.961110.xyz/posts/2533.html">Docker源码分析(四)—— Docker Daemon 启动流程</a></li><li><a href="https://blog.961110.xyz/posts/21441.html">Docker源码分析(五)—— 命令 docker inspect 的执行</a></li><li><a href="https://blog.961110.xyz/posts/40974.html">Docker源码分析(六)—— 命令 docker stop 的执行</a></li><li><a href="https://blog.961110.xyz/posts/37106.html">Docker源码分析(七)—— 命令 docker kill的执行</a></li><li><a href="https://blog.961110.xyz/posts/20332.html">Docker源码分析(八)—— 命令 docker restart 的执行</a></li><li><a href="https://blog.961110.xyz/posts/52812.html">Docker源码分析(九)—— 命令 docker pause 的执行</a></li><li><a href="https://blog.961110.xyz/posts/49360.html">Docker源码分析(十)—— 命令 docker unpause 的执行</a></li><li><a href="https://blog.961110.xyz/posts/24743.html">Docker源码分析(十一)—— 命令 docker rm 的执行</a></li></ol><h2 id="container-生命周期"><a href="#container-生命周期" class="headerlink" title="container 生命周期"></a>container 生命周期</h2><table> <tr> <td ><center><img src="https://images.961110.xyz:5001/i/2021/11/29/225c067867401b13496dcbc407f9747e.png" >图1 container 生命周期简图 </center></td> <td ><center><img src="https://images.961110.xyz:5001/i/2021/11/29/2fe10549b3c93adcf3e25e7d0cff707e.png" >图2 container 生命周期图</center></td> </tr></table><table><thead><tr><th><strong>条件</strong></th><th><strong>对应 container 生命周期</strong></th><th><strong>备注</strong></th></tr></thead><tbody><tr><td>docker container create</td><td>created</td><td></td></tr><tr><td>docker container start</td><td>(created | stopped) –> running</td><td></td></tr><tr><td>docker container run</td><td>none –> created –> running</td><td></td></tr><tr><td>docker container stop</td><td>running –> stopped</td><td></td></tr><tr><td>docker container restart</td><td>running –> running</td><td></td></tr><tr><td>docker container pause</td><td>running –> paused</td><td></td></tr><tr><td>docker container unpause</td><td>paused –> running</td><td></td></tr><tr><td>docker container kill</td><td>running –> stopped</td><td></td></tr><tr><td>docker container rm</td><td>(created | stopped) –> deleted</td><td></td></tr><tr><td>docker container inspect</td><td>none</td><td>获取容器的详细信息</td></tr><tr><td>OOM</td><td>running –> (stopped | running)</td><td>当 OOM 时容器会被 kill 掉,<br />然后根据策略退出或重启。</td></tr><tr><td>container process exited</td><td>running –> (stopped | running)</td><td>当容器中的进程退出时,<br />根据策略进行重启或者 stop。</td></tr></tbody></table><p></p>]]></content>
<categories>
<category> Docker </category>
</categories>
<tags>
<tag> Docker </tag>
<tag> 笔记 </tag>
<tag> 毕设 </tag>
<tag> 源码分析 </tag>
</tags>
</entry>
<entry>
<title>Docker源码分析(五)—— 命令 docker inspect 的执行</title>
<link href="/posts/21441.html"/>
<url>/posts/21441.html</url>
<content type="html"><![CDATA[<blockquote><p>博文分析的 Docker 源码基于 19.03 版本,阅读工具采用 Goland,追踪的是<code>docker inspect</code>命令的执行过程。</p><p>第一次阅读 Docker 源码,如有纰漏还请轻喷,喷完希望能够通过邮件或者留言的方式指出问题。</p><p>限于篇幅以及关注重点的原因,部分个人认为不重要的代码会用省略号代替。</p><p>Tips: 如果图片看不清楚可以点击查看大图。</p></blockquote><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>想要获取 container 的相关信息,看到有 <code>docker inspect containerID</code> 这个命令而且返回了较为完备的 container 底层信息,所以对其进行简单的追踪。这个命令是 Docker Client 的命令,所以从 Docker Client 开始分析。</p><h2 id="Docker-Client"><a href="#Docker-Client" class="headerlink" title="Docker Client"></a>Docker Client</h2><p>调用链路是 <code>main() --> runDocker() --> newDockerCommand() --> AddCommands() --> cmd.AddCommand() --> container.NewContainerCommand() --> cmd.AddCommand() --> newInspectCommand()</code></p><p>从<code>newInspectCommand()</code>开始进行分析,其他的都已经在 <a href="https://blog.961110.xyz/posts/64829.html">Docker源码分析(一)—— docker run 命令在 Docker Client 中的执行</a> 一文中详细分析过。</p><h3 id="newInspectCommand-dockerCli"><a href="#newInspectCommand-dockerCli" class="headerlink" title="newInspectCommand(dockerCli)"></a>newInspectCommand(dockerCli)</h3><p>cli/command/container/inspect.go: 19</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">newInspectCommand</span><span class="params">(dockerCli command.Cli)</span> *<span class="title">cobra</span>.<span class="title">Command</span></span> {</span><br><span class="line"><span class="keyword">var</span> opts inspectOptions</span><br><span class="line"></span><br><span class="line">cmd := &cobra.Command{</span><br><span class="line">Use: <span class="string">"inspect [OPTIONS] CONTAINER [CONTAINER...]"</span>,</span><br><span class="line">Short: <span class="string">"Display detailed information on one or more containers"</span>,</span><br><span class="line">Args: cli.RequiresMinArgs(<span class="number">1</span>),</span><br><span class="line">RunE: <span class="function"><span class="keyword">func</span><span class="params">(cmd *cobra.Command, args []<span class="keyword">string</span>)</span> <span class="title">error</span></span> {</span><br><span class="line">opts.refs = args</span><br><span class="line"><span class="keyword">return</span> runInspect(dockerCli, opts)</span><br><span class="line">},</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">flags := cmd.Flags()</span><br><span class="line">flags.StringVarP(&opts.format, <span class="string">"format"</span>, <span class="string">"f"</span>, <span class="string">""</span>, <span class="string">"Format the output using the given Go template"</span>)</span><br><span class="line">flags.BoolVarP(&opts.size, <span class="string">"size"</span>, <span class="string">"s"</span>, <span class="literal">false</span>, <span class="string">"Display total file sizes"</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> cmd</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个函数的主要作用是新增 <code>docker inspect</code> 这个命令,相关的可视化效果可以在下图看到。</p><img src="https://images.961110.xyz:5001/i/2021/11/28/docker-inspect--help.png" alt="image-20211128171139806" style="zoom:50%;" /><h3 id="runInspect-dockerCli-opts"><a href="#runInspect-dockerCli-opts" class="headerlink" title="runInspect(dockerCli, opts)"></a>runInspect(dockerCli, opts)</h3><p>cli/command/container/inspect.go: 39</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">runInspect</span><span class="params">(dockerCli command.Cli, opts inspectOptions)</span> <span class="title">error</span></span> {</span><br><span class="line">client := dockerCli.Client()</span><br><span class="line">ctx := context.Background()</span><br><span class="line"></span><br><span class="line">getRefFunc := <span class="function"><span class="keyword">func</span><span class="params">(ref <span class="keyword">string</span>)</span> <span class="params">(<span class="keyword">interface</span>{}, []<span class="keyword">byte</span>, error)</span></span> {</span><br><span class="line"><span class="keyword">return</span> client.ContainerInspectWithRaw(ctx, ref, opts.size)</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRefFunc)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li><code>dockerCli</code>是 Docker 命令行的对象;</li><li><code>dockerCli.Client()</code>是与 docker server(Docker Daemon) 交互的 API Client,对应的结构体中存放着服务端的地址、通信方式(unix. fd, tcp)等信息。</li><li><code>getRefFunc</code>这个函数是这个命令的核心函数,其发送请求 get 请求来获取指定 container 的 JSON 信息,这个下面再分析。</li><li><code>inspect.Inspect()</code>这个函数 的作用是将<code>getRefFunc</code>得到的 JSON 格式的信息输出到 Docker Client 的输出端。</li></ul><h3 id="client-ContainerInspectWithRaw-ctx-ref-opts-size"><a href="#client-ContainerInspectWithRaw-ctx-ref-opts-size" class="headerlink" title="client.ContainerInspectWithRaw(ctx, ref, opts.size)"></a>client.ContainerInspectWithRaw(ctx, ref, opts.size)</h3><p>github.com/docker/docker/client/container_inspect.go: 30</p><p>从调用路径中可以看出,这里引用的是 Docker Daemon 的实现</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(cli *Client)</span> <span class="title">ContainerInspectWithRaw</span><span class="params">(ctx context.Context, containerID <span class="keyword">string</span>, getSize <span class="keyword">bool</span>)</span> <span class="params">(types.ContainerJSON, []<span class="keyword">byte</span>, error)</span></span> {</span><br><span class="line"><span class="keyword">if</span> containerID == <span class="string">""</span> {</span><br><span class="line"><span class="keyword">return</span> types.ContainerJSON{}, <span class="literal">nil</span>, objectNotFoundError{object: <span class="string">"container"</span>, id: containerID}</span><br><span class="line">}</span><br><span class="line">query := url.Values{}</span><br><span class="line"><span class="keyword">if</span> getSize {</span><br><span class="line">query.Set(<span class="string">"size"</span>, <span class="string">"1"</span>)</span><br><span class="line">}</span><br><span class="line">serverResp, err := cli.get(ctx, <span class="string">"/containers/"</span>+containerID+<span class="string">"/json"</span>, query, <span class="literal">nil</span>)</span><br><span class="line"><span class="keyword">defer</span> ensureReaderClosed(serverResp)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">return</span> types.ContainerJSON{}, <span class="literal">nil</span>, wrapResponseError(err, serverResp, <span class="string">"container"</span>, containerID)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">body, err := ioutil.ReadAll(serverResp.body)</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> {</span><br><span class="line"><span class="keyword">return</span> types.ContainerJSON{}, <span class="literal">nil</span>, err</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> response types.ContainerJSON</span><br><span class="line">rdr := bytes.NewReader(body)</span><br><span class="line">err = json.NewDecoder(rdr).Decode(&response)</span><br><span class="line"><span class="keyword">return</span> response, body, err</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>最核心的是这行代码</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line">serverResp, err := cli.get(ctx, <span class="string">"/containers/"</span>+containerID+<span class="string">"/json"</span>, query, <span class="literal">nil</span>)</span><br></pre></td></tr></table></figure><p>向 Docker Daemon 发送 get 请求,并获取响应。</p><h3 id="流程图"><a href="#流程图" class="headerlink" title="流程图"></a>流程图</h3><img src="https://images.961110.xyz:5001/i/2021/11/28/docker-inspect--Docker-Client-.png" style="zoom: 50%;" /><p>接下来看 Docker Daemon 的处理过程。</p><h2 id="Docker-Daemon"><a href="#Docker-Daemon" class="headerlink" title="Docker Daemon"></a>Docker Daemon</h2><p>Docker Daemon 这一端从 <code>containerRouter.initRoutes()</code>开始分析(Docker Daemon 其他的分析可以在 <a href="https://blog.961110.xyz/posts/19054.html">之前的文章</a> 中查阅),这个函数填充了 container 相关的一些 API 的路由了逻辑,其中就包括了 <code>docker inspect</code>这个命令。</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(r *containerRouter)</span> <span class="title">initRoutes</span><span class="params">()</span></span> {</span><br><span class="line">r.routes = []router.Route{</span><br><span class="line">...</span><br><span class="line">router.NewGetRoute(<span class="string">"/containers/{name:.*}/json"</span>, r.getContainersByName),</span><br><span class="line">...</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>r.getContainersByName</code>调用的是<code>containerRouter.backend.ContainerInspect()</code>,进而根据 API 版本进行不同的函数调用,在 1.20 以上的版本调用的是 <code>daemon.ContainerInspectCurrent()</code>,就执行到了一个需要关注的函数</p><h3 id="Daemon-ContainerInspectCurrent"><a href="#Daemon-ContainerInspectCurrent" class="headerlink" title="Daemon.ContainerInspectCurrent()"></a>Daemon.ContainerInspectCurrent()</h3><p>该函数处理流程图如下</p><img src="https://images.961110.xyz:5001/i/2021/11/29/ContainerInspectCurrent-.png" style="zoom: 50%;" /><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(daemon *Daemon)</span> <span class="title">ContainerInspectCurrent</span><span class="params">(name <span class="keyword">string</span>, size <span class="keyword">bool</span>)</span> <span class="params">(*types.ContainerJSON, error)</span></span> {</span><br><span class="line"></span><br><span class="line"><span class="comment">// 根据输入参数获取对应 container 结构体对象</span></span><br><span class="line">container, err := daemon.GetContainer(name)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 获取指定 container 的基本信息</span></span><br><span class="line">base, err := daemon.getInspectData(container)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 获取指定 container 的 Endpoint 配置信息</span></span><br><span class="line">apiNetworks := <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="keyword">string</span>]*networktypes.EndpointSettings)</span><br><span class="line"><span class="keyword">for</span> name, epConf := <span class="keyword">range</span> container.NetworkSettings.Networks {</span><br><span class="line">...</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 获取指定 container 的 挂载点 配置信息</span></span><br><span class="line">mountPoints := container.GetMountPoints()</span><br><span class="line"></span><br><span class="line"><span class="comment">// 获取指定 container 的网络配置信息</span></span><br><span class="line">networkSettings := &types.NetworkSettings{</span><br><span class="line">...</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">...</span><br><span class="line"><span class="comment">// 组装成 JSON 信息返回</span></span><br><span class="line"><span class="keyword">return</span> &types.ContainerJSON{</span><br><span class="line">ContainerJSONBase: base,</span><br><span class="line">Mounts: mountPoints,</span><br><span class="line">Config: container.Config,</span><br><span class="line">NetworkSettings: networkSettings,</span><br><span class="line">}, <span class="literal">nil</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>daemon.GetContainer(name)</code>这个函数的参数名叫做 name,但是 name 实际上可能是容器名、容器完整的 ID、容器 ID 的前缀这三种可能,<code>GetContainer()</code>内部对这三种情况进行了单独处理,最后还是通过容器 ID 来获取对应的容器(容器这个结构体的对象)</p><h3 id="Daemon-GetContainer"><a href="#Daemon-GetContainer" class="headerlink" title="Daemon.GetContainer()"></a>Daemon.GetContainer()</h3><p>daemon/container.go: 38</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(daemon *Daemon)</span> <span class="title">GetContainer</span><span class="params">(prefixOrName <span class="keyword">string</span>)</span> <span class="params">(*container.Container, error)</span></span> {</span><br><span class="line"></span><br><span class="line"><span class="comment">// 省略掉的就是上面提到的分三种情况处理的代码,最后通过 containerID 来获取 container 对象</span></span><br><span class="line">...</span><br><span class="line"><span class="keyword">return</span> daemon.containers.Get(containerID), <span class="literal">nil</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里的 <code>Get()</code> 方法实现于 container/memory_store.go: 29,其实现思路是根据容器 ID 在 memoryStore 中获取 ID 对应的容器对象,memoryStore 的实现如下</p><figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="keyword">type</span> memoryStore <span class="keyword">struct</span> {</span><br><span class="line">s <span class="keyword">map</span>[<span class="keyword">string</span>]*Container</span><br><span class="line">sync.RWMutex</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>就是一个简单的 map,保存在内存中。</p><h3 id="流程图-1"><a href="#流程图-1" class="headerlink" title="流程图"></a>流程图</h3><img src="https://images.961110.xyz:5001/i/2021/11/29/docker-inspect--Docker-Daemon-.png" style="zoom:50%;" /><h2 id="docker-inspect-命令执行流程图"><a href="#docker-inspect-命令执行流程图" class="headerlink" title="docker inspect 命令执行流程图"></a>docker inspect 命令执行流程图</h2><img src="https://images.961110.xyz:5001/i/2021/11/29/docker-inspect-.png" style="zoom:50%;" /><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><p>无</p>]]></content>
<categories>
<category> Docker </category>
</categories>
<tags>
<tag> Docker </tag>
<tag> 笔记 </tag>
<tag> 毕设 </tag>
<tag> 源码分析 </tag>
</tags>
</entry>
<entry>
<title>编码开发规范</title>
<link href="/posts/41554.html"/>
<url>/posts/41554.html</url>
<content type="html"><![CDATA[<h2 id="项目命名规范"><a href="#项目命名规范" class="headerlink" title="项目命名规范"></a>项目命名规范</h2><ol><li>项目名称用汉字,而项目 URL 不要用拼音,尽量用英文</li><li>项目 URL 不用大写、下划线</li><li>项目 URL 不用有歧义的缩写,如 src, led</li></ol><h2 id="代码库命名"><a href="#代码库命名" class="headerlink" title="代码库命名"></a>代码库命名</h2><p>英文:首先找官方的翻译</p><p><img src="https://images.961110.xyz:5001/i/2021/11/22/01c9badceb5f05852709a733e7ae9211.png" alt="image-20211122144217905"></p><h2 id="Git-规范"><a href="#Git-规范" class="headerlink" title="Git 规范"></a>Git 规范</h2><h3 id="分支规范"><a href="#分支规范" class="headerlink" title="分支规范"></a>分支规范</h3><p>每个需求一个分支</p><h4 id="Git-Flow"><a href="#Git-Flow" class="headerlink" title="Git Flow"></a>Git Flow</h4><p>都是双分支结构,一个开发分支,一个主分支。</p><h5 id="简易-Git-Flow"><a href="#简易-Git-Flow" class="headerlink" title="简易 Git Flow"></a>简易 Git Flow</h5><p><img src="https://images.961110.xyz:5001/i/2021/11/22/Git-Flow.png" alt="image-20211122144655829"></p><h5 id="严格版简易-Git-Flow"><a href="#严格版简易-Git-Flow" class="headerlink" title="严格版简易 Git Flow"></a>严格版简易 Git Flow<img src="https://images.961110.xyz:5001/i/2021/11/22/Git-Flowca311f5bf49e8ac1.png" alt="image-20211122144757206"></h5><h4 id="分支命名"><a href="#分支命名" class="headerlink" title="分支命名"></a>分支命名</h4><p>命名以分支类型开头+数字+简易描述</p><p><img src="https://images.961110.xyz:5001/i/2021/11/22/49134bf300d2a7631eb132607102bf49.png" alt="image-20211122144848896"></p><h2 id="Git-add-规范"><a href="#Git-add-规范" class="headerlink" title="Git add 规范"></a>Git add 规范</h2><ol><li>依赖包通过网络安装,进行依赖管理,不可以直接提交第三方包</li><li>build 的结果也不要提交上去</li><li>未发布到仓库的依赖包,可以创建一个仓库,放入需要的依赖包</li><li>不用的代码直接删除,需要的时候通过 git 历史找回</li></ol><h2 id="Git-commit-规范"><a href="#Git-commit-规范" class="headerlink" title="Git commit 规范"></a>Git commit 规范</h2><p> 使用 git cz 工具</p><p>原子性提交:每次提交都是可回滚的(一个需求可以提交好几次)</p><h2 id="Git-push-规范"><a href="#Git-push-规范" class="headerlink" title="Git push 规范"></a>Git push 规范</h2><p>主分支</p><ul><li>禁止强制推送(push –force)</li><li>禁止篡改历史(reset),只可以 revert</li></ul><p>开发分支</p><ul><li>允许强制推送</li><li>允许 rebase</li></ul><h2 id="代码合并规范"><a href="#代码合并规范" class="headerlink" title="代码合并规范"></a>代码合并规范</h2><ul><li>直接合并会产生一条垃圾历史</li><li>压缩合并(Squash)适合合并无关痛痒的 commit,以合并时的 commit message 为准</li><li>rebase 不产生垃圾历史</li></ul><h2 id="Git-tag-规范"><a href="#Git-tag-规范" class="headerlink" title="Git tag 规范"></a>Git tag 规范</h2><p>版本格式:主版本号.次版本号.修订号</p><ol><li>主版本号(Major):不兼容的修改,</li><li>次版本号(Minor):向下兼容的功能性新增,</li><li>修订号(Patch):向下兼容的问题修正。</li></ol><p>是否需要前缀 v: 看个人喜好</p><p><img src="https://images.961110.xyz:5001/i/2021/11/22/Git-tag-sample.png" alt="image-20211122155615007"></p><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><ol><li><a href="https://www.bilibili.com/video/BV18q4y1n7Bd?p=1&share_medium=iphone&share_plat=ios&share_session_id=43521A93-AADA-4B7D-B501-C2B8DC28DB0A&share_source=COPY&share_tag=s_i&timestamp=1637545301&unique_k=NuF4QQb">https://www.bilibili.com/video/BV18q4y1n7Bd?p=1&share_medium=iphone&share_plat=ios&share_session_id=43521A93-AADA-4B7D-B501-C2B8DC28DB0A&share_source=COPY&share_tag=s_i&timestamp=1637545301&unique_k=NuF4QQb</a></li></ol>]]></content>
<categories>
<category> 教程 </category>
</categories>
<tags>
<tag> 教程 </tag>
</tags>
</entry>
<entry>
<title>【转载】中文文案排版指北</title>
<link href="/posts/39632.html"/>
<url>/posts/39632.html</url>
<content type="html"><![CDATA[<blockquote><p>【转载】原文地址 <a href="https://www.quchao.net/Chinese-Copywriting-Guidelines.html">www.quchao.net</a></p></blockquote><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>日常生活、写博、工作、学习中,我们每个人都会遇到文案排版问题。<br>借着新的一年,往后都延用本文新规范来写作,有机会我会再写一下整体大纲结构以及配色突出。</p><p>统一中文文案、排版的相关用法,降低团队成员 / 访客之间的沟通阅读成本,增加网站气质。</p><hr><h2 id="空格"><a href="#空格" class="headerlink" title="空格"></a>空格</h2><p>「有研究显示,打字的时候不喜欢在中文和英文之间加空格的人,感情路都走得很辛苦,有七成的比例会在 34 岁的时候跟自己不爱的人结婚,而其余三成的人最后只能把遗产留给自己的猫。 毕竟爱情跟书写都需要适时地留白。 与大家共勉之。」 ——<a href="https://github.com/vinta/pangu.js">vinta/paranoid-auto-spacing</a></p><h3 id="中英文之间需要增加空格"><a href="#中英文之间需要增加空格" class="headerlink" title="中英文之间需要增加空格"></a>中英文之间需要增加空格</h3><p>正确:</p><blockquote><p>在 LeanCloud 上,数据存储是围绕 <code>AVObject</code> 进行的。</p></blockquote><p>错误:</p><blockquote><p>在 LeanCloud 上,数据存储是围绕<code>AVObject</code>进行的。</p><p>在 LeanCloud 上,数据存储是围绕<code>AVObject</code> 进行的。</p></blockquote><p>完整的正确用法:</p><blockquote><p>在 LeanCloud 上,数据存储是围绕 <code>AVObject</code> 进行的。每个 <code>AVObject</code> 都包含了与 JSON 兼容的 key-value 对应的数据。数据是 schema-free 的,你不需要在每个 <code>AVObject</code> 上提前指定存在哪些键,只要直接设定对应的 key-value 即可。</p></blockquote><p>例外:「豆瓣FM」等产品名词,按照官方所定义的格式书写。</p><h3 id="中文与数字之间需要增加空格"><a href="#中文与数字之间需要增加空格" class="headerlink" title="中文与数字之间需要增加空格"></a>中文与数字之间需要增加空格</h3><p>正确:</p><blockquote><p>今天出去买菜花了 5000 元。</p></blockquote><p>错误:</p><blockquote><p>今天出去买菜花了 5000元。</p><p>今天出去买菜花了5000 元。</p></blockquote><h3 id="数字与单位之间需要增加空格"><a href="#数字与单位之间需要增加空格" class="headerlink" title="数字与单位之间需要增加空格"></a>数字与单位之间需要增加空格</h3><p>正确:</p><blockquote><p>我家的光纤入屋宽带有 10 Gbps,SSD 一共有 20 TB</p></blockquote><p>错误:</p><blockquote><p>我家的光纤入屋宽带有 10Gbps,SSD 一共有 20TB</p></blockquote><p>例外:度 / 百分比与数字之间不需要增加空格。</p><p>正确:</p><blockquote><p>今天是 233° 的高温。</p><p>新 MacBook Pro 有 15% 的 CPU 性能提升。</p></blockquote><p>错误:</p><blockquote><p>今天是 233 ° 的高温。</p><p>新 MacBook Pro 有 15 % 的 CPU 性能提升。</p></blockquote><h3 id="全角标点与其他字符之间不加空格"><a href="#全角标点与其他字符之间不加空格" class="headerlink" title="全角标点与其他字符之间不加空格"></a>全角标点与其他字符之间不加空格</h3><p>正确:</p><blockquote><p>刚刚买了一部 iPhone,好开心!</p></blockquote><p>错误:</p><blockquote><p>刚刚买了一部 iPhone ,好开心!</p><p>刚刚买了一部 iPhone, 好开心!</p></blockquote><h4 id="用-text-spacing-来挽救?"><a href="#用-text-spacing-来挽救?" class="headerlink" title="用 text-spacing 来挽救?"></a>用 <code>text-spacing</code> 来挽救?</h4><p>CSS Text Module Level 4 的 <a href="https://www.w3.org/TR/css-text-4/#text-spacing-property"><code>text-spacing</code></a> 和 Microsoft 的 <a href="https://msdn.microsoft.com/library/ms531164(v=vs.85).aspx"><code>-ms-text-autospace</code></a> 可以实现自动为中英文之间增加空白。不过目前并未普及,另外在其他应用场景,例如 macOS、iOS、Windows 等用户介面目前并不存在这个特性,所以请继续保持随手加空格的习惯。</p><hr><h2 id="标点符号"><a href="#标点符号" class="headerlink" title="标点符号"></a>标点符号</h2><h3 id="不重复使用标点符号"><a href="#不重复使用标点符号" class="headerlink" title="不重复使用标点符号"></a>不重复使用标点符号</h3><p>正确:</p><blockquote><p>德国队竟然战胜了巴西队!</p><p>她竟然对你说「喵」?!</p></blockquote><p>错误:</p><blockquote><p>德国队竟然战胜了巴西队!!</p><p>德国队竟然战胜了巴西队!!!!!!!!</p><p>她竟然对你说「喵」??!!</p><p>她竟然对你说「喵」?!?!??!!</p></blockquote><hr><h2 id="全角和半角"><a href="#全角和半角" class="headerlink" title="全角和半角"></a>全角和半角</h2><p>不明白什么是全角(全形)与半角(半形)符号?请查看维基百科词条『<a href="https://zh.wikipedia.org/wiki/%E5%85%A8%E5%BD%A2%E5%92%8C%E5%8D%8A%E5%BD%A2">全形和半形</a>』。</p><h3 id="使用全角中文标点"><a href="#使用全角中文标点" class="headerlink" title="使用全角中文标点"></a>使用全角中文标点</h3><p>正确:</p><blockquote><p>嗨!你知道嘛?今天前台的小妹跟我说「喵」了哎!</p><p>核磁共振成像(NMRI)是什么原理都不知道?JFGI!</p></blockquote><p>错误:</p><blockquote><p>嗨! 你知道嘛? 今天前台的小妹跟我说 “喵” 了哎!</p><p>嗨! 你知道嘛?今天前台的小妹跟我说 “喵” 了哎!</p><p>核磁共振成像 (NMRI) 是什么原理都不知道? JFGI!</p><p>核磁共振成像(NMRI)是什么原理都不知道?JFGI!</p></blockquote><h3 id="数字使用半角字符"><a href="#数字使用半角字符" class="headerlink" title="数字使用半角字符"></a>数字使用半角字符</h3><p>正确:</p><blockquote><p>这个蛋糕只卖 1000 元。</p></blockquote><p>错误:</p><blockquote><p>这个蛋糕只卖 1000 元。</p></blockquote><p>例外:在设计稿、宣传海报中如出现极少量数字的情形时,为方便文字对齐,是可以使用全形数字的。</p><h3 id="遇到完整的英文整句、特殊名词,其内容使用半角标点"><a href="#遇到完整的英文整句、特殊名词,其内容使用半角标点" class="headerlink" title="遇到完整的英文整句、特殊名词,其内容使用半角标点"></a>遇到完整的英文整句、特殊名词,其内容使用半角标点</h3><p>正确:</p><blockquote><p>贾伯斯那句话是怎么说的?「Stay hungry, stay foolish.」</p><p>推荐你阅读《Hackers & Painters: Big Ideas from the Computer Age》,非常的有趣。</p></blockquote><p>错误:</p><blockquote><p>贾伯斯那句话是怎么说的?「Stay hungry,stay foolish。」</p><p>推荐你阅读《Hackers&Painters:Big Ideas from the Computer Age》,非常的有趣。</p></blockquote><hr><h2 id="名词"><a href="#名词" class="headerlink" title="名词"></a>名词</h2><h3 id="专有名词使用正确的大小写"><a href="#专有名词使用正确的大小写" class="headerlink" title="专有名词使用正确的大小写"></a>专有名词使用正确的大小写</h3><p>大小写相关用法原属于英文书写范畴,不属于本 wiki 讨论内容,在这里只对部分易错用法进行简述。</p><p>正确:</p><blockquote><p>使用 GitHub 登录</p><p>我们的客户有 GitHub、Foursquare、Microsoft Corporation、Google、Facebook, Inc.。</p></blockquote><p>错误:</p><blockquote><p>使用 github 登录</p><p>使用 GITHUB 登录</p><p>使用 Github 登录</p><p>使用 gitHub 登录</p><p>使用 gイんĤЦ8 登录</p><p>我们的客户有 github、foursquare、microsoft corporation、google、facebook, inc.。</p><p>我们的客户有 GITHUB、FOURSQUARE、MICROSOFT CORPORATION、GOOGLE、FACEBOOK, INC.。</p><p>我们的客户有 Github、FourSquare、MicroSoft Corporation、Google、FaceBook, Inc.。</p><p>我们的客户有 gitHub、fourSquare、microSoft Corporation、google、faceBook, Inc.。</p><p>我们的客户有 gイんĤЦ8、キouЯƧquムгє、๓เςг๏ร๏Ŧt ς๏гק๏гคtเ๏ภn、900913、ƒ4ᄃëв๏๏к, IПᄃ.。</p></blockquote><p>注意:当网页中需要配合整体视觉风格而出现全部大写/小写的情形,HTML 中请使用标淮的大小写规范进行书写;并通过 <code>text-transform: uppercase;</code>/<code>text-transform: lowercase;</code> 对表现形式进行定义。</p><h3 id="不要使用不地道的缩写"><a href="#不要使用不地道的缩写" class="headerlink" title="不要使用不地道的缩写"></a>不要使用不地道的缩写</h3><p>正确:</p><blockquote><p>我们需要一位熟悉 JavaScript、HTML5,至少理解一种框架(如 Backbone.js、AngularJS、React 等)的前端开发者。</p></blockquote><p>错误:</p><blockquote><p>我们需要一位熟悉 Js、h5,至少理解一种框架(如 backbone、angular、RJS 等)的 FED。</p></blockquote><hr><h2 id="争议"><a href="#争议" class="headerlink" title="争议"></a>争议</h2><p>以下用法略带有个人色彩,即:无论是否遵循下述规则,从语法的角度来讲都是<strong>正确</strong>的。</p><h3 id="链接之间增加空格"><a href="#链接之间增加空格" class="headerlink" title="链接之间增加空格"></a>链接之间增加空格</h3><p>用法:</p><blockquote><p>请 <a href="#">提交一个 issue</a> 并分配给相关同事。</p><p>访问我们网站的最新动态,请 <a href="#">点击这里</a> 进行订阅!</p></blockquote><p>对比用法:</p><blockquote><p>请<a href="#">提交一个 issue</a>并分配给相关同事。</p><p>访问我们网站的最新动态,请<a href="#">点击这里</a>进行订阅!</p></blockquote><h3 id="简体中文使用直角引号"><a href="#简体中文使用直角引号" class="headerlink" title="简体中文使用直角引号"></a>简体中文使用直角引号</h3><p>用法:</p><blockquote><p>「老师,『有条不紊』的『紊』是什么意思?」</p></blockquote><p>对比用法:</p><blockquote><p>“老师,‘有条不紊’的‘紊’是什么意思?”</p></blockquote><hr><h2 id="工具"><a href="#工具" class="headerlink" title="工具"></a>工具</h2><table><thead><tr><th>仓库</th><th>语言</th></tr></thead><tbody><tr><td><a href="https://github.com/vinta/paranoid-auto-spacing" target="_blank">vinta/paranoid-auto-spacing<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>JavaScript</td></tr><tr><td><a href="https://github.com/huei90/pangu.node" target="_blank">huei90/pangu.node<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>Node.js</td></tr><tr><td><a href="https://github.com/huacnlee/auto-correct" target="_blank">huacnlee/auto-correct<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>Ruby</td></tr><tr><td><a href="https://github.com/sparanoid/space-lover" target="_blank">sparanoid/space-lover<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>PHP (WordPress)</td></tr><tr><td><a href="https://github.com/NauxLiu/auto-correct" target="_blank">nauxliu/auto-correct<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>PHP</td></tr><tr><td><a href="https://github.com/jxlwqq/chinese-typesetting" target="_blank">jxlwqq/chinese-typesetting<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>PHP</td></tr><tr><td><a href="https://github.com/hotoo/pangu.vim" target="_blank">hotoo/pangu.vim<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>Vim</td></tr><tr><td><a href="https://github.com/sparanoid/grunt-auto-spacing" target="_blank">sparanoid/grunt-auto-spacing<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>Node.js (Grunt)</td></tr><tr><td><a href="https://github.com/hjiang/scripts/blob/master/add-space-between-latin-and-cjk" target="_blank">hjiang/scripts/add-space-between-latin-and-cjk<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>Python</td></tr><tr><td><a href="https://github.com/hustcc/hint" target="_blank">hustcc/hint<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>Python</td></tr><tr><td><a href="https://github.com/studygolang/autocorrect" target="_blank">studygolang/autocorrect<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>Go</td></tr></tbody></table><hr><h2 id="谁在这样做?"><a href="#谁在这样做?" class="headerlink" title="谁在这样做?"></a>谁在这样做?</h2><table><thead><tr><th>网站</th><th>文案</th><th>UGC</th></tr></thead><tbody><tr><td><a href="https://www.apple.com/cn/" target="_blank">Apple 中国<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>是</td><td>N/A</td></tr><tr><td><a href="https://www.apple.com/hk/" target="_blank">Apple 香港<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>是</td><td>N/A</td></tr><tr><td><a href="https://www.apple.com/tw/" target="_blank">Apple 台湾<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>是</td><td>N/A</td></tr><tr><td><a href="https://www.microsoft.com/zh-cn/" target="_blank">Microsoft 中国<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>是</td><td>N/A</td></tr><tr><td><a href="https://www.microsoft.com/zh-hk/" target="_blank">Microsoft 香港<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>是</td><td>N/A</td></tr><tr><td><a href="https://www.microsoft.com/zh-tw/" target="_blank">Microsoft 台湾<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>是</td><td>N/A</td></tr><tr><td><a href="https://leancloud.cn/" target="_blank">LeanCloud<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>是</td><td>N/A</td></tr><tr><td><a href="https://www.v2ex.com/" target="_blank">V2EX<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>是</td><td>是</td></tr><tr><td><a href="https://apple4us.com/" target="_blank">Apple4us<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>是</td><td>N/A</td></tr><tr><td><a href="https://ruby-china.org/" target="_blank">Ruby China<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>是</td><td>标题达成</td></tr><tr><td><a href="https://phphub.org/" target="_blank">PHPHub<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>是</td><td>标题达成</td></tr><tr><td><a href="https://sspai.com/" target="_blank">少数派<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke:currentColor;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg></a></td><td>是</td><td>N/A</td></tr></tbody></table><hr><h2 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h2><ul><li> <a href="https://www.thoughtco.com/guidelines-for-using-capital-letters-1691724">Guidelines for Using Capital Letters - ThoughtCo.</a></li><li> <a href="https://en.wikipedia.org/wiki/Letter_case">Letter case - Wikipedia</a></li><li> <a href="https://en.oxforddictionaries.com/grammar/punctuation">Punctuation - Oxford Dictionaries</a></li><li> <a href="https://owl.english.purdue.edu/owl/section/1/6/">Punctuation - The Purdue OWL</a></li><li> <a href="https://www.wikihow.com/Use-English-Punctuation-Correctly">How to Use English Punctuation Correctly - wikiHow</a></li><li> <a href="https://zh.opensuse.org/index.php?title=Help:%E6%A0%BC%E5%BC%8F">格式 - openSUSE</a></li><li> <a href="https://zh.wikipedia.org/wiki/%E5%85%A8%E5%BD%A2%E5%92%8C%E5%8D%8A%E5%BD%A2">全形和半形 - 维基百科</a></li><li> <a href="https://zh.wikipedia.org/wiki/%E5%BC%95%E8%99%9F">引号 - 维基百科</a></li><li> <a href="https://zh.wikipedia.org/wiki/%E7%96%91%E5%95%8F%E9%A9%9A%E5%98%86%E8%99%9F">疑问惊叹号 - 维基百科</a></li></ul>]]></content>
<categories>
<category> 教程 </category>
</categories>
<tags>
<tag> 教程 </tag>
</tags>