forked from ericzhang-cn/blog.codinglabs.org
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrss.xml
1841 lines (1717 loc) · 186 KB
/
rss.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"?>
<rss version="2.0">
<channel>
<title>CodingLabs</title>
<link>http://blog.codinglabs.org</link>
<description>keep coding, keep foolish</description>
<lastBuildDate>Tue, 19 Jan 2016 12:11:46 +0800</lastBuildDate>
<language>zh-cn</language>
<item>
<title>如何实现一个malloc</title>
<link>http://blog.codinglabs.org/articles/a-malloc-tutorial.html?utm_source=rss&utm_medium=rss&utm_campaign=rss</link>
<guid>http://blog.codinglabs.org/articles/a-malloc-tutorial.html</guid>
<author>[email protected] 张洋</author>
<pubDate>Tue, 19 Aug 2014 00:00:00 +0800</pubDate>
<description><p>任何一个用过或学过C的人对malloc都不会陌生。大家都知道malloc可以分配一段连续的内存空间,并且在不再使用时可以通过free释放掉。但是,许多程序员对malloc背后的事情并不熟悉,许多人甚至把malloc当做操作系统所提供的系统调用或C的关键字。实际上,malloc只是C的标准库中提供的一个普通函数,而且实现malloc的<strong>基本</strong>思想并不复杂,任何一个对C和操作系统有些许了解的程序员都可以很容易理解。</p>
<p>这篇文章通过实现一个简单的malloc来描述malloc背后的机制。当然与现有C的标准库实现(例如glibc)相比,我们实现的malloc并不是特别高效,但是这个实现比目前真实的malloc实现要简单很多,因此易于理解。重要的是,这个实现和真实实现在基本原理上是一致的。</p>
<p>这篇文章将首先介绍一些所需的基本知识,如操作系统对进程的内存管理以及相关的系统调用,然后逐步实现一个简单的malloc。为了简单起见,这篇文章将只考虑x86_64体系结构,操作系统为Linux。</p>
<!-- toc -->
<h1 id="1-什么是malloc">1 什么是malloc</h1>
<p>在实现malloc之前,先要相对正式地对malloc做一个定义。</p>
<p>根据标准C库函数的定义,malloc具有如下原型:</p>
<pre class="prettyprint linenums lang-c">void* malloc(size_t size);
</pre>
<p>这个函数要实现的功能是在系统中分配一段连续的可用的内存,具体有如下要求:</p>
<ul>
<li>malloc分配的内存大小<strong>至少</strong>为size参数所指定的字节数</li>
<li>malloc的返回值是一个指针,指向一段可用内存的起始地址</li>
<li>多次调用malloc所分配的地址不能有重叠部分,除非某次malloc所分配的地址被释放掉</li>
<li>malloc应该尽快完成内存分配并返回(不能使用<a href="http://en.wikipedia.org/wiki/NP-hard">NP-hard</a>的内存分配算法)</li>
<li>实现malloc时应同时实现内存大小调整和内存释放函数(即realloc和free)</li>
</ul>
<p>对于malloc更多的说明可以在命令行中键入以下命令查看:</p>
<pre class="prettyprint linenums lang-bash">man malloc
</pre>
<h1 id="2-预备知识">2 预备知识</h1>
<p>在实现malloc之前,需要先解释一些Linux系统内存相关的知识。</p>
<h2 id="21-linux内存管理">2.1 Linux内存管理</h2>
<h3 id="211-虚拟内存地址与物理内存地址">2.1.1 虚拟内存地址与物理内存地址</h3>
<p>为了简单,现代操作系统在处理内存地址时,普遍采用虚拟内存地址技术。即在汇编程序(或机器语言)层面,当涉及内存地址时,都是使用虚拟内存地址。采用这种技术时,每个进程仿佛自己独享一片$2^N$字节的内存,其中$N$是机器位数。例如在64位CPU和64位操作系统下,每个进程的虚拟地址空间为$2^{64}$Byte。</p>
<p>这种虚拟地址空间的作用主要是简化程序的编写及方便操作系统对进程间内存的隔离管理,真实中的进程不太可能(也用不到)如此大的内存空间,实际能用到的内存取决于物理内存大小。</p>
<p>由于在机器语言层面都是采用虚拟地址,当实际的机器码程序涉及到内存操作时,需要根据当前进程运行的实际上下文将虚拟地址转换为物理内存地址,才能实现对真实内存数据的操作。这个转换一般由一个叫<a href="http://en.wikipedia.org/wiki/Memory_management_unit">MMU</a>(Memory Management Unit)的硬件完成。</p>
<h3 id="212-页与地址构成">2.1.2 页与地址构成</h3>
<p>在现代操作系统中,不论是虚拟内存还是物理内存,都不是以字节为单位进行管理的,而是以页(Page)为单位。一个内存页是一段固定大小的连续内存地址的总称,具体到Linux中,典型的内存页大小为4096Byte(4K)。</p>
<p>所以内存地址可以分为页号和页内偏移量。下面以64位机器,4G物理内存,4K页大小为例,虚拟内存地址和物理内存地址的组成如下:</p>
<p><img src="http://blog-codinglabs-org.qiniudn.com/image/a-malloc-tutorial-01.png" alt="内存地址构成"></p>
<p>上面是虚拟内存地址,下面是物理内存地址。由于页大小都是4K,所以页内便宜都是用低12位表示,而剩下的高地址表示页号。</p>
<p>MMU映射单位并不是字节,而是页,这个映射通过查一个常驻内存的数据结构<a href="http://en.wikipedia.org/wiki/Page_table">页表</a>来实现。现在计算机具体的内存地址映射比较复杂,为了加快速度会引入一系列缓存和优化,例如<a href="http://en.wikipedia.org/wiki/Translation_lookaside_buffer">TLB</a>等机制。下面给出一个经过简化的内存地址翻译示意图,虽然经过了简化,但是基本原理与现代计算机真实的情况的一致的。</p>
<p><img src="http://blog-codinglabs-org.qiniudn.com/image/a-malloc-tutorial-02.png" alt="内存地址翻译"></p>
<h3 id="213-内存页与磁盘页">2.1.3 内存页与磁盘页</h3>
<p>我们知道一般将内存看做磁盘的的缓存,有时MMU在工作时,会发现页表表明某个内存页不在物理内存中,此时会触发一个缺页异常(Page Fault),此时系统会到磁盘中相应的地方将磁盘页载入到内存中,然后重新执行由于缺页而失败的机器指令。关于这部分,因为可以看做对malloc实现是透明的,所以不再详细讲述,有兴趣的可以参考《深入理解计算机系统》相关章节。</p>
<p>最后附上一张在维基百科找到的更加符合真实地址翻译的流程供大家参考,这张图加入了TLB和缺页异常的流程(<a href="http://en.wikipedia.org/wiki/Page_table">图片来源页</a>)。</p>
<p><img src="http://blog-codinglabs-org.qiniudn.com/image/a-malloc-tutorial-03.png" alt="较为完整的地址翻译流程"></p>
<h2 id="22-linux进程级内存管理">2.2 Linux进程级内存管理</h2>
<h3 id="221-内存排布">2.2.1 内存排布</h3>
<p>明白了虚拟内存和物理内存的关系及相关的映射机制,下面看一下具体在一个进程内是如何排布内存的。</p>
<p>以Linux 64位系统为例。理论上,64bit内存地址可用空间为0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF,这是个相当庞大的空间,Linux实际上只用了其中一小部分(256T)。</p>
<p>根据<a href="https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt">Linux内核相关文档</a>描述,Linux64位操作系统仅使用低47位,高17位做扩展(只能是全0或全1)。所以,实际用到的地址为空间为0x0000000000000000 ~ 0x00007FFFFFFFFFFF和0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF,其中前面为用户空间(User Space),后者为内核空间(Kernel Space)。图示如下:</p>
<p><img src="http://blog-codinglabs-org.qiniudn.com/image/a-malloc-tutorial-04.png" alt="Linux进程地址排布"></p>
<p>对用户来说,主要关注的空间是User Space。将User Space放大后,可以看到里面主要分为如下几段:</p>
<ul>
<li>Code:这是整个用户空间的最低地址部分,存放的是指令(也就是程序所编译成的可执行机器码)</li>
<li>Data:这里存放的是初始化过的全局变量</li>
<li>BSS:这里存放的是未初始化的全局变量</li>
<li>Heap:堆,这是我们本文重点关注的地方,堆自低地址向高地址增长,后面要讲到的brk相关的系统调用就是从这里分配内存</li>
<li>Mapping Area:这里是与mmap系统调用相关的区域。大多数实际的malloc实现会考虑通过mmap分配较大块的内存区域,本文不讨论这种情况。这个区域自高地址向低地址增长</li>
<li>Stack:这是栈区域,自高地址向低地址增长</li>
</ul>
<p>下面我们主要关注Heap区域的操作。对整个Linux内存排布有兴趣的同学可以参考其它资料。</p>
<h3 id="222-heap内存模型">2.2.2 Heap内存模型</h3>
<p>一般来说,malloc所申请的内存主要从Heap区域分配(本文不考虑通过mmap申请大块内存的情况)。</p>
<p>由上文知道,进程所面对的虚拟内存地址空间,只有按页映射到物理内存地址,才能真正使用。受物理存储容量限制,整个堆虚拟内存空间不可能全部映射到实际的物理内存。Linux对堆的管理示意如下:</p>
<p><img src="http://blog-codinglabs-org.qiniudn.com/image/a-malloc-tutorial-05.png" alt="Linux进程堆管理"></p>
<p>Linux维护一个break指针,这个指针指向堆空间的某个地址。从堆起始地址到break之间的地址空间为映射好的,可以供进程访问;而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错。</p>
<h3 id="223-brk与sbrk">2.2.3 brk与sbrk</h3>
<p>由上文知道,要增加一个进程实际的可用堆大小,就需要将break指针向高地址移动。Linux通过brk和sbrk系统调用操作break指针。两个系统调用的原型如下:</p>
<pre class="prettyprint linenums lang-c">int brk(void *addr);
void *sbrk(intptr_t increment);
</pre>
<p>brk将break指针直接设置为某个地址,而sbrk将break从当前位置移动increment所指定的增量。brk在执行成功时返回0,否则返回-1并设置errno为ENOMEM;sbrk成功时返回break移动之前所指向的地址,否则返回(void *)-1。</p>
<p>一个小技巧是,如果将increment设置为0,则可以获得当前break的地址。</p>
<p>另外需要注意的是,由于Linux是按页进行内存映射的,所以如果break被设置为没有按页大小对齐,则系统实际上会在最后映射一个完整的页,从而实际已映射的内存空间比break指向的地方要大一些。但是使用break之后的地址是很危险的(尽管也许break之后确实有一小块可用内存地址)。</p>
<h3 id="224-资源限制与rlimit">2.2.4 资源限制与rlimit</h3>
<p>系统对每一个进程所分配的资源不是无限的,包括可映射的内存空间,因此每个进程有一个rlimit表示当前进程可用的资源上限。这个限制可以通过getrlimit系统调用得到,下面代码获取当前进程虚拟内存空间的rlimit:</p>
<pre class="prettyprint linenums lang-c">int main() {
struct rlimit *limit = (struct rlimit *)malloc(sizeof(struct rlimit));
getrlimit(RLIMIT_AS, limit);
printf("soft limit: %ld, hard limit: %ld\n", limit->rlim_cur, limit->rlim_max);
}
</pre>
<p>其中rlimit是一个结构体:</p>
<pre class="prettyprint linenums lang-c">struct rlimit {
rlim_t rlim_cur; /* Soft limit */
rlim_t rlim_max; /* Hard limit (ceiling for rlim_cur) */
};
</pre>
<p>每种资源有软限制和硬限制,并且可以通过setrlimit对rlimit进行有条件设置。其中硬限制作为软限制的上限,非特权进程只能设置软限制,且不能超过硬限制。</p>
<h1 id="3-实现malloc">3 实现malloc</h1>
<h2 id="31-玩具实现">3.1 玩具实现</h2>
<p>在正式开始讨论malloc的实现前,我们可以利用上述知识实现一个简单但几乎没法用于真实的玩具malloc,权当对上面知识的复习:</p>
<pre class="prettyprint linenums lang-c">/* 一个玩具malloc */
#include <sys/types.h>
#include <unistd.h>
void *malloc(size_t size)
{
void *p;
p = sbrk(0);
if (sbrk(size) == (void *)-1)
return NULL;
return p;
}
</pre>
<p>这个malloc每次都在当前break的基础上增加size所指定的字节数,并将之前break的地址返回。这个malloc由于对所分配的内存缺乏记录,不便于内存释放,所以无法用于真实场景。</p>
<h2 id="32-正式实现">3.2 正式实现</h2>
<p>下面严肃点讨论malloc的实现方案。</p>
<h3 id="321-数据结构">3.2.1 数据结构</h3>
<p>首先我们要确定所采用的数据结构。一个简单可行方案是将堆内存空间以块(Block)的形式组织起来,每个块由meta区和数据区组成,meta区记录数据块的元信息(数据区大小、空闲标志位、指针等等),数据区是真实分配的内存区域,并且数据区的第一个字节地址即为malloc返回的地址。</p>
<p>可以用如下结构体定义一个block:</p>
<pre class="prettyprint linenums lang-c">typedef struct s_block *t_block;
struct s_block {
size_t size; /* 数据区大小 */
t_block next; /* 指向下个块的指针 */
int free; /* 是否是空闲块 */
int padding; /* 填充4字节,保证meta块长度为8的倍数 */
char data[1] /* 这是一个虚拟字段,表示数据块的第一个字节,长度不应计入meta */
};
</pre>
<p>由于我们只考虑64位机器,为了方便,我们在结构体最后填充一个int,使得结构体本身的长度为8的倍数,以便内存对齐。示意图如下:</p>
<p><img src="http://blog-codinglabs-org.qiniudn.com/image/a-malloc-tutorial-06.png" alt="Block结构"></p>
<h3 id="322-寻找合适的block">3.2.2 寻找合适的block</h3>
<p>现在考虑如何在block链中查找合适的block。一般来说有两种查找算法:</p>
<ul>
<li><strong>First fit</strong>:从头开始,使用第一个数据区大小大于要求size的块所谓此次分配的块</li>
<li><strong>Best fit</strong>:从头开始,遍历所有块,使用数据区大小大于size且差值最小的块作为此次分配的块</li>
</ul>
<p>两种方法各有千秋,best fit具有较高的内存使用率(payload较高),而first fit具有更好的运行效率。这里我们采用first fit算法。</p>
<pre class="prettyprint linenums lang-c">/* First fit */
t_block find_block(t_block *last, size_t size) {
t_block b = first_block;
while(b && !(b->free && b->size >= size)) {
*last = b;
b = b->next;
}
return b;
}
</pre>
<p>find_block从frist_block开始,查找第一个符合要求的block并返回block起始地址,如果找不到这返回NULL。这里在遍历时会更新一个叫last的指针,这个指针始终指向当前遍历的block。这是为了如果找不到合适的block而开辟新block使用的,具体会在接下来的一节用到。</p>
<h3 id="323-开辟新的block">3.2.3 开辟新的block</h3>
<p>如果现有block都不能满足size的要求,则需要在链表最后开辟一个新的block。这里关键是如何只使用sbrk创建一个struct:</p>
<pre class="prettyprint linenums lang-c">#define BLOCK_SIZE 24 /* 由于存在虚拟的data字段,sizeof不能正确计算meta长度,这里手工设置 */
t_block extend_heap(t_block last, size_t s) {
t_block b;
b = sbrk(0);
if(sbrk(BLOCK_SIZE + s) == (void *)-1)
return NULL;
b->size = s;
b->next = NULL;
if(last)
last->next = b;
b->free = 0;
return b;
}
</pre>
<h3 id="324-分裂block">3.2.4 分裂block</h3>
<p>First fit有一个比较致命的缺点,就是可能会让很小的size占据很大的一块block,此时,为了提高payload,应该在剩余数据区足够大的情况下,将其分裂为一个新的block,示意如下:</p>
<p><img src="http://blog-codinglabs-org.qiniudn.com/image/a-malloc-tutorial-07.png" alt="分裂block"></p>
<p>实现代码:</p>
<pre class="prettyprint linenums lang-c">void split_block(t_block b, size_t s) {
t_block new;
new = b->data + s;
new->size = b->size - s - BLOCK_SIZE ;
new->next = b->next;
new->free = 1;
b->size = s;
b->next = new;
}
</pre>
<h3 id="325-malloc的实现">3.2.5 malloc的实现</h3>
<p>有了上面的代码,我们可以利用它们整合成一个简单但初步可用的malloc。注意首先我们要定义个block链表的头first_block,初始化为NULL;另外,我们需要剩余空间至少有BLOCK_SIZE + 8才执行分裂操作。</p>
<p>由于我们希望malloc分配的数据区是按8字节对齐,所以在size不为8的倍数时,我们需要将size调整为大于size的最小的8的倍数:</p>
<pre class="prettyprint linenums lang-c">size_t align8(size_t s) {
if(s & 0x7 == 0)
return s;
return ((s >> 3) + 1) << 3;
}
</pre>
<pre class="prettyprint linenums lang-c">#define BLOCK_SIZE 24
void *first_block=NULL;
/* other functions... */
void *malloc(size_t size) {
t_block b, last;
size_t s;
/* 对齐地址 */
s = align8(size);
if(first_block) {
/* 查找合适的block */
last = first_block;
b = find_block(&last, s);
if(b) {
/* 如果可以,则分裂 */
if ((b->size - s) >= ( BLOCK_SIZE + 8))
split_block(b, s);
b->free = 0;
} else {
/* 没有合适的block,开辟一个新的 */
b = extend_heap(last, s);
if(!b)
return NULL;
}
} else {
b = extend_heap(NULL, s);
if(!b)
return NULL;
first_block = b;
}
return b->data;
}
</pre>
<h3 id="326-calloc的实现">3.2.6 calloc的实现</h3>
<p>有了malloc,实现calloc只要两步:</p>
<ol>
<li>malloc一段内存</li>
<li>将数据区内容置为0</li>
</ol>
<p>由于我们的数据区是按8字节对齐的,所以为了提高效率,我们可以每8字节一组置0,而不是一个一个字节设置。我们可以通过新建一个size_t指针,将内存区域强制看做size_t类型来实现。</p>
<pre class="prettyprint linenums lang-c">void *calloc(size_t number, size_t size) {
size_t *new;
size_t s8, i;
new = malloc(number * size);
if(new) {
s8 = align8(number * size) >> 3;
for(i = 0; i < s8; i++)
new[i] = 0;
}
return new;
}
</pre>
<h3 id="327-free的实现">3.2.7 free的实现</h3>
<p>free的实现并不像看上去那么简单,这里我们要解决两个关键问题:</p>
<ol>
<li>如何验证所传入的地址是有效地址,即确实是通过malloc方式分配的数据区首地址</li>
<li>如何解决碎片问题</li>
</ol>
<p>首先我们要保证传入free的地址是有效的,这个有效包括两方面:</p>
<ul>
<li>地址应该在之前malloc所分配的区域内,即在first_block和当前break指针范围内</li>
<li>这个地址确实是之前通过我们自己的malloc分配的</li>
</ul>
<p>第一个问题比较好解决,只要进行地址比较就可以了,关键是第二个问题。这里有两种解决方案:一是在结构体内埋一个magic number字段,free之前通过相对偏移检查特定位置的值是否为我们设置的magic number,另一种方法是在结构体内增加一个magic pointer,这个指针指向数据区的第一个字节(也就是在合法时free时传入的地址),我们在free前检查magic pointer是否指向参数所指地址。这里我们采用第二种方案:</p>
<p>首先我们在结构体中增加magic pointer(同时要修改BLOCK_SIZE):</p>
<pre class="prettyprint linenums lang-c">typedef struct s_block *t_block;
struct s_block {
size_t size; /* 数据区大小 */
t_block next; /* 指向下个块的指针 */
int free; /* 是否是空闲块 */
int padding; /* 填充4字节,保证meta块长度为8的倍数 */
void *ptr; /* Magic pointer,指向data */
char data[1] /* 这是一个虚拟字段,表示数据块的第一个字节,长度不应计入meta */
};
</pre>
<p>然后我们定义检查地址合法性的函数:</p>
<pre class="prettyprint linenums lang-c">t_block get_block(void *p) {
char *tmp;
tmp = p;
return (p = tmp -= BLOCK_SIZE);
}
int valid_addr(void *p) {
if(first_block) {
if(p > first_block && p < sbrk(0)) {
return p == (get_block(p))->ptr;
}
}
return 0;
}
</pre>
<p>当多次malloc和free后,整个内存池可能会产生很多碎片block,这些block很小,经常无法使用,甚至出现许多碎片连在一起,虽然总体能满足某此malloc要求,但是由于分割成了多个小block而无法fit,这就是碎片问题。</p>
<p>一个简单的解决方式时当free某个block时,如果发现它相邻的block也是free的,则将block和相邻block合并。为了满足这个实现,需要将s_block改为双向链表。修改后的block结构如下:</p>
<pre class="prettyprint linenums lang-c">typedef struct s_block *t_block;
struct s_block {
size_t size; /* 数据区大小 */
t_block prev; /* 指向上个块的指针 */
t_block next; /* 指向下个块的指针 */
int free; /* 是否是空闲块 */
int padding; /* 填充4字节,保证meta块长度为8的倍数 */
void *ptr; /* Magic pointer,指向data */
char data[1] /* 这是一个虚拟字段,表示数据块的第一个字节,长度不应计入meta */
};
</pre>
<p>合并方法如下:</p>
<pre class="prettyprint linenums lang-c">t_block fusion(t_block b) {
if (b->next && b->next->free) {
b->size += BLOCK_SIZE + b->next->size;
b->next = b->next->next;
if(b->next)
b->next->prev = b;
}
return b;
}
</pre>
<p>有了上述方法,free的实现思路就比较清晰了:首先检查参数地址的合法性,如果不合法则不做任何事;否则,将此block的free标为1,并且在可以的情况下与后面的block进行合并。如果当前是最后一个block,则回退break指针释放进程内存,如果当前block是最后一个block,则回退break指针并设置first_block为NULL。实现如下:</p>
<pre class="prettyprint linenums lang-c">void free(void *p) {
t_block b;
if(valid_addr(p)) {
b = get_block(p);
b->free = 1;
if(b->prev && b->prev->free)
b = fusion(b->prev);
if(b->next)
fusion(b);
else {
if(b->prev)
b->prev->prev = NULL;
else
first_block = NULL;
brk(b);
}
}
}
</pre>
<h3 id="328-realloc的实现">3.2.8 realloc的实现</h3>
<p>为了实现realloc,我们首先要实现一个内存复制方法。如同calloc一样,为了效率,我们以8字节为单位进行复制:</p>
<pre class="prettyprint linenums lang-c">void copy_block(t_block src, t_block dst) {
size_t *sdata, *ddata;
size_t i;
sdata = src->ptr;
ddata = dst->ptr;
for(i = 0; (i * 8) < src->size && (i * 8) < dst->size; i++)
ddata[i] = sdata[i];
}
</pre>
<p>然后我们开始实现realloc。一个简单(但是低效)的方法是malloc一段内存,然后将数据复制过去。但是我们可以做的更高效,具体可以考虑以下几个方面:</p>
<ul>
<li>如果当前block的数据区大于等于realloc所要求的size,则不做任何操作</li>
<li>如果新的size变小了,考虑split</li>
<li>如果当前block的数据区不能满足size,但是其后继block是free的,并且合并后可以满足,则考虑做合并</li>
</ul>
<p>下面是realloc的实现:</p>
<pre class="prettyprint linenums lang-c">void *realloc(void *p, size_t size) {
size_t s;
t_block b, new;
void *newp;
if (!p)
/* 根据标准库文档,当p传入NULL时,相当于调用malloc */
return malloc(size);
if(valid_addr(p)) {
s = align8(size);
b = get_block(p);
if(b->size >= s) {
if(b->size - s >= (BLOCK_SIZE + 8))
split_block(b,s);
} else {
/* 看是否可进行合并 */
if(b->next && b->next->free
&& (b->size + BLOCK_SIZE + b->next->size) >= s) {
fusion(b);
if(b->size - s >= (BLOCK_SIZE + 8))
split_block(b, s);
} else {
/* 新malloc */
newp = malloc (s);
if (!newp)
return NULL;
new = get_block(newp);
copy_block(b, new);
free(p);
return(newp);
}
}
return (p);
}
return NULL;
}
</pre>
<h2 id="33-遗留问题和优化">3.3 遗留问题和优化</h2>
<p>以上是一个较为简陋,但是初步可用的malloc实现。还有很多遗留的可能优化点,例如:</p>
<ul>
<li>同时兼容32位和64位系统</li>
<li>在分配较大快内存时,考虑使用mmap而非sbrk,这通常更高效</li>
<li>可以考虑维护多个链表而非单个,每个链表中的block大小均为一个范围内,例如8字节链表、16字节链表、24-32字节链表等等。此时可以根据size到对应链表中做分配,可以有效减少碎片,并提高查询block的速度</li>
<li>可以考虑链表中只存放free的block,而不存放已分配的block,可以减少查找block的次数,提高效率</li>
</ul>
<p>还有很多可能的优化,这里不一一赘述。下面附上一些参考文献,有兴趣的同学可以更深入研究。</p>
<h1 id="4-其它参考">4 其它参考</h1>
<ol>
<li>这篇文章大量参考了<a href="http://www.inf.udec.cl/~leo/Malloc_tutorial.pdf">A malloc Tutorial</a>,其中一些图片和代码直接引用了文中的内容,这里特别指出</li>
<li><a href="http://csapp.cs.cmu.edu/">Computer Systems: A Programmer's Perspective, 2/E</a>一书有许多值得参考的地方</li>
<li>关于Linux的虚拟内存模型,<a href="http://duartes.org/gustavo/blog/post/anatomy-of-a-program-in-memory/">Anatomy of a Program in Memory</a>是很好的参考资料,另外作者还有一篇<a href="http://duartes.org/gustavo/blog/post/how-the-kernel-manages-your-memory/">How the Kernel Manages Your Memory</a>对于Linux内核中虚拟内存管理的部分有很好的讲解</li>
<li>对于真实世界的malloc实现,可以参考<a href="http://repo.or.cz/w/glibc.git/blob/HEAD:/malloc/malloc.c">glibc的实现</a></li>
<li>本文写作过程中大量参考了<a href="http://www.wikipedia.org/">维基百科</a>,再次感谢这个伟大的网站,并且呼吁大家在手头允许的情况下可以适当捐助维基百科,帮助这个造福人类的系统运行下去</li>
</ol>
</description>
</item>
<item>
<title>生成特定分布随机数的方法</title>
<link>http://blog.codinglabs.org/articles/methods-for-generating-random-number-distributions.html?utm_source=rss&utm_medium=rss&utm_campaign=rss</link>
<guid>http://blog.codinglabs.org/articles/methods-for-generating-random-number-distributions.html</guid>
<author>[email protected] 张洋</author>
<pubDate>Sat, 14 Jun 2014 00:00:00 +0800</pubDate>
<description><p>生成随机数是程序设计里常见的需求。一般的编程语言都会自带一个随机数生成函数,用于生成服从均匀分布的随机数。不过有时需要生成服从其它分布的随机数,例如高斯分布或指数分布等。有些编程语言已经有比较完善的实现,例如Python的NumPy。这篇文章介绍如何通过均匀分布随机数生成函数生成符合特定概率分布的随机数,主要介绍Inverse Ttransform和Acceptance-Rejection两种基础算法以及一些相关的衍生方法。下文我们均假设已经拥有一个可以生成0到1之间均匀分布的随机数生成函数,关于如何生成均匀分布等更底层的随机数生成理论,请参考其它资料,本文不做讨论。</p>
<!-- toc -->
<h1 id="基础算法">基础算法</h1>
<h2 id="inverse-transform-method">Inverse Transform Method</h2>
<p>最简单的生成算法是Inverse Transform Method(下文简称ITM)。如果我们可以给出概率分布的累积分布函数(下文简称CDF)及其逆函数的解析表达式,则可以非常简单便捷的生成指定分布随机数。</p>
<h3 id="itm算法描述">ITM算法描述</h3>
<blockquote>
<ol>
<li>生成一个服从均匀分布的随机数\(U \sim Uni(0,1)\)</li>
<li>设\(F(X)\)为指定分布的CDF,\(F^{-1}(Y)\)是其逆函数。返回\(X=F^{-1}(U)\)作为结果</li>
</ol>
</blockquote>
<h3 id="itm算法说明">ITM算法说明</h3>
<p>这是一个非常简洁高效的算法,下面说明其原理及正确性。</p>
<p>我们通过图示可以更直观的明白算法的原理。下图是某概率分布的CDF:</p>
<p class="picture"><img alt="" src="/uploads/pictures/methods-for-generating-random-number-distributions/inverse-transformation.png"/></p>
<p>我们从横轴上标注两点\(x_a\)和\(x_b\),其CDF值分别为\(F(x_a)\)和\(F(x_b)\)。</p>
<p>由于U服从0到1之间的均匀分布,因此对于一次U的取样,U落入\(F(x_a)\)和\(F(x_b)\)之间的概率为:</p>
<p>\[P(U \in (F(x_a),F(x_b))) = F(x_b) - F(x_a)\]</p>
<p>而由于CDF都是单调非减函数,因此这个概率同时等于\(X\)落入\(x_a\)和\(x_b\)之间的概率,即:</p>
<p>\[P(U \in (F(x_a),F(x_b))) = P(F^{-1}(U) \in (F^{-1}(F(x_a)),F^{-1}(F(x_b)))) = P(X \in (x_a,x_b)) = F(x_b) - F(x_a)\]</p>
<p>而根据CDF的定义,这刚好说明\(X\)服从以\(F(x)\)为CDF的分布,因此我们的生成算法是正确的。</p>
<h3 id="itm实现示例">ITM实现示例</h3>
<p>下面以<a href="http://en.wikipedia.org/wiki/Exponential_distribution">指数分布</a>为例说明如何应用ITM。</p>
<p>首先我们需要求解CDF的逆函数。我们知道指数分布的CDF为</p>
<p>\[F(X)=1-e^{-\lambda X}\]</p>
<p>通过简单的代数运算,可以得到其逆函数为</p>
<p>\[F^{-1}(Y)=-\frac{1}{\lambda}\ln(1-Y)\]</p>
<p>由于\(U\)服从从0到1的均匀分布蕴含着\(1-U\)服从同样的分布,因此在实际实现时可以用\(Y\)代替\(1-Y\),得到:</p>
<p>\[F^{-1}(Y)=-\frac{1}{\lambda}\ln(Y)\]</p>
<p>下面给出一个Python的实现示例程序。</p>
<pre class="prettyprint linenums lang-python">import random
import math
def exponential_rand(lam):
if lam <= 0:
return -1
U = random.uniform(0.0, 1.0)
return (-1.0 / lam) * math.log(U)
</pre>
<h2 id="acceptance-rejection-method">Acceptance-Rejection Method</h2>
<p>一般来说ITM是一种很好的算法,简单且高效,如果可以使用的话,是第一选择。但是ITM有自身的局限性,就是要求必须能给出CDF逆函数的解析表达式,有些时候要做到这点比较困难,这限制了ITM的适用范围。</p>
<p>当无法给出CDF逆函数的解析表达式时,Acceptance-Rejection Method(下文简称ARM)是另外的选择。ARM的适用范围比ITM要大,只要给出概率密度函数(下文简称PDF)的解析表达式即可,而大多数常用分布的PDF是可以查到的。</p>
<h3 id="arm算法描述">ARM算法描述</h3>
<blockquote>
<ol>
<li>设PDF为\(f(x)\)。首先生成一个均匀分布随机数\(X \sim Uni(x_{min},x_{max})\)</li>
<li>独立的生成另一个均匀分布随机数\(Y \sim Uni(y_{min},y_{max})\)</li>
<li>如果\(Y \leq f(X)\),则返回\(X\),否则回到第1步</li>
</ol>
</blockquote>
<h3 id="arm算法说明">ARM算法说明</h3>
<p>通过一幅图可以清楚的看到ARM的工作原理。</p>
<p class="picture"><img alt="" src="/uploads/pictures/methods-for-generating-random-number-distributions/accept-reject.png"/></p>
<p>ARM本质上是一种模拟方法,而非直接数学方法。它每次生成新的随机数后,通过另一个随机数来保证其被接受概率服从指定的PDF。</p>
<p>显然ARM从效率上不如ITM,但是其适应性更广,在无法得到CDF的逆函数时,ARM是不错的选择。</p>
<h3 id="arm实现示例">ARM实现示例</h3>
<p>下面使用ARM实现一个能产生<a href="http://en.wikipedia.org/wiki/Gauss_distribution">标准正态分布</a>的随机数生成函数。</p>
<p>首先我们要得到标准正态分布的PDF,其数学表示为:</p>
<p>\[f(x)=\frac{1}{\sqrt{2\pi}}e^{-\frac{x^2}{2}}\]</p>
<p>为了方便,这里我会直接使用<a href="http://www.scipy.org/">SciPy</a>来计算其PDF。</p>
<p>程序如下。</p>
<pre class="prettyprint linenums lang-python">import random
import scipy.stats as ss
def standard_normal_rand():
while True:
X = random.uniform(-3.0,3.0)
Y = random.uniform(0.0, 0.5)
if Y < ss.norm.pdf(X):
return X
</pre>
<p><strong>注意</strong>:标准正态分布的x取值范围从理论上说是\((-\infty,\infty)\),但是当离开均值点很远后,其概率密度可忽略不计。这里只取\((-3.0,3.0)\),实际使用时可以根据具体需要扩大这个取值范围。</p>
<h1 id="衍生算法">衍生算法</h1>
<h2 id="组合算法">组合算法</h2>
<p>当目标分布可以用其它分布经过四则运算表示时,可以使用组合算法生成对应随机数。</p>
<p>最常见的就是某分布可以表示成多个独立同分布(下文简称IID)随机变量之和。例如二项分布可以表示成多个0-1分布之和,<a href="http://en.wikipedia.org/wiki/Erlang_distribution">Erlang分布</a>可以由多个IID的指数分布得出。</p>
<p>以Erlang分布为例说明如何生成这类随机数。</p>
<p>设\(X_1,X_2,\cdots,X_k\)为服从0到1均匀分布的IID随机数,则\(-\frac{1}{\lambda}lnX_1,-\frac{1}{\lambda}lnX_2,\cdots,-\frac{1}{\lambda}lnX_k\)为服从指数分布的IID随机数,因此</p>
<p>\[X=-\frac{1}{\lambda}lnX_1-\frac{1}{\lambda}lnX_2-\cdots-\frac{1}{\lambda}lnX_k=-\frac{1}{\lambda}ln\prod_{i=1}^k{X_i}\sim Erl(k,\lambda)\]</p>
<p>所以生成Erlang分布随机数的算法如下:</p>
<blockquote>
<ol>
<li>生成\(X_1,X_2,\cdots,X_k\sim Uni(0,1)\)</li>
<li>返回\(-\frac{1}{\lambda}ln\prod_{i=1}^k{X_i}\)</li>
</ol>
</blockquote>
<p>这类分布的随机数生成算法很直观,就是先生成相关的n个IID随机数,然后带入简单求和公式或其它四则公式得出最终随机数。其数学理论基础是<a href="http://en.wikipedia.org/wiki/Convolution">卷积理论</a>,稍微有些复杂,这里不再讨论,有兴趣的同学可以查阅相关资料。</p>
<h2 id="生成具有相关性的随机数">生成具有相关性的随机数</h2>
<p>现在考虑生成多维随机数,以最简单的二维随机数为例。</p>
<p>如果两个维度的随机数是相互独立的,那么只要分别生成两个列就可以了。但是如果要求两列具有一定的相关系数,则需要做一些特殊处理。</p>
<p>下列算法可以生成两列具有相关系数\(\rho\)的随机数。</p>
<blockquote>
<ol>
<li>生成IID随机变量\(X\)和\(Y\)</li>
<li>计算\(X'=\rho X+\sqrt{1-\rho^2}Y\)</li>
<li>返回\((X,X')\)</li>
</ol>
</blockquote>
<p>可以这样验证其正确性:</p>
<p>\[corr(X,X')=\rho corr(X,X)+\sqrt{1-\rho^2}corr(X,Y)=\rho\]</p>
<p><strong>注意</strong>:\(corr(X,X)=1\),\(corr(X,Y)=0\)。</p>
<p>因此\(X\)和\(X'\)确实具有相关系数\(\rho\)。</p>
<h1 id="更多参考">更多参考</h1>
<p>这篇文章讨论了生成指定分布随机数的基本方法。这篇文章只打算讨论基础方法,所以还有很多有趣的内容,本文没有深入的探讨。这里给出一些扩展阅读资料,供有兴趣的朋友深入学习。首先是一篇<a href="http://ftp.arl.mil/random/random.pdf">非常好的文档</a>,这篇文章来自美国陆军实验室,对计算机生成指定分布随机数的方方面面进行了全面深入描述,是不可多得的好资料。</p>
<p>在实现方面,可以参考<a href="https://github.com/numpy/numpy/blob/master/numpy/random/mtrand/distributions.c">NumPy中关于random的实现</a>以及我开发的<a href="https://github.com/ericzhang-cn/random.js">JavaScript实现</a>。另外我做过一个<a href="http://blog.codinglabs.org/demo/distributions.html">不同概率分布的可视化页面</a>,可以帮助你直观理解不同分布及PDF参数对分布的影响。</p>
</description>
</item>
<item>
<title>2048-AI程序算法分析</title>
<link>http://blog.codinglabs.org/articles/2048-ai-analysis.html?utm_source=rss&utm_medium=rss&utm_campaign=rss</link>
<guid>http://blog.codinglabs.org/articles/2048-ai-analysis.html</guid>
<author>[email protected] 张洋</author>
<pubDate>Fri, 04 Apr 2014 00:00:00 +0800</pubDate>
<description><p>针对目前火爆的<a href="http://gabrielecirulli.github.io/2048/">2048</a>游戏,<a href="http://ov3y.github.io/2048-AI/">有人实现了一个AI程序</a>,可以以较大概率(高于90%)赢得游戏,并且<a href="http://stackoverflow.com/questions/22342854/what-is-the-optimal-algorithm-for-the-game-2048">作者在stackoverflow上简要介绍了AI的算法框架和实现思路</a>。但是这个回答主要集中在启发函数的选取上,对AI用到的核心算法并没有仔细说明。这篇文章将主要分为两个部分,第一部分介绍其中用到的基础算法,即Minimax和Alpha-beta剪枝;第二部分分析作者具体的实现。</p>
<p class="picture"><img alt="" src="/uploads/pictures/2048-ai-analysis/00.png"/></p>
<h1 id="基础算法">基础算法</h1>
<p>2048本质上可以抽象成信息对称双人对弈模型(玩家向四个方向中的一个移动,然后计算机在某个空格中填入2或4)。这里“信息对称”是指在任一时刻对弈双方对格局的信息完全一致,移动策略仅依赖对接下来格局的推理。作者使用的核心算法为对弈模型中常用的带Alpha-beta剪枝的Minimax。这个算法也常被用于如国际象棋等信息对称对弈AI中。</p>
<h2 id="minimax">Minimax</h2>
<p>下面先介绍不带剪枝的Minimax。首先本文将通过一个简单的例子说明Minimax算法的思路和决策方式。</p>
<h3 id="问题">问题</h3>
<p>现在考虑这样一个游戏:有三个盘子A、B和C,每个盘子分别放有三张纸币。A放的是1、20、50;B放的是5、10、100;C放的是1、5、20。单位均为“元”。有甲、乙两人,两人均对三个盘子和上面放置的纸币有可以任意查看。游戏分三步:</p>
<ol>
<li>甲从三个盘子中选取一个。</li>
<li>乙从甲选取的盘子中拿出两张纸币交给甲。</li>
<li>甲从乙所给的两张纸币中选取一张,拿走。</li>
</ol>
<p>其中甲的目标是最后拿到的纸币面值尽量大,乙的目标是让甲最后拿到的纸币面值尽量小。</p>
<p>下面用Minimax算法解决这个问题。</p>
<h3 id="基本思路">基本思路</h3>
<p>一般解决博弈类问题的自然想法是将格局组织成一棵树,树的每一个节点表示一种格局,而父子关系表示由父格局经过一步可以到达子格局。Minimax也不例外,它通过对以当前格局为根的格局树搜索来确定下一步的选择。而一切格局树搜索算法的核心都是对每个格局价值的评价。Minimax算法基于以下朴素思想确定格局价值:</p>
<ul>
<li>Minimax是一种悲观算法,即假设对手每一步都会将我方引入从当前看理论上价值最小的格局方向,即对手具有完美决策能力。因此我方的策略应该是选择那些对方所能达到的让我方最差情况中最好的,也就是让对方在完美决策下所对我造成的损失最小。</li>
<li>Minimax不找理论最优解,因为理论最优解往往依赖于对手是否足够愚蠢,Minimax中我方完全掌握主动,如果对方每一步决策都是完美的,则我方可以达到预计的最小损失格局,如果对方没有走出完美决策,则我方可能达到比预计的最悲观情况更好的结局。总之我方就是要在最坏情况中选择最好的。</li>
</ul>
<p>上面的表述有些抽象,下面看具体示例。</p>
<h3 id="解题">解题</h3>
<p>下图是上述示例问题的格局树:</p>
<p class="picture"><img alt="" src="/uploads/pictures/2048-ai-analysis/01.png"/></p>
<p>注意,由于示例问题格局数非常少,我们可以给出完整的格局树。这种情况下我可以找到Minimax算法的全局最优解。而真实情况中,格局树非常庞大,即使是计算机也不可能给出完整的树,因此我们往往只搜索一定深度,这时只能找到局部最优解。</p>
<p>我们从甲的角度考虑。其中正方形节点表示轮到我方(甲),而三角形表示轮到对方(乙)。经过三轮对弈后(我方-对方-我方),将进入终局。黄色叶结点表示所有可能的结局。从甲方看,由于最终的收益可以通过纸币的面值评价,我们自然可以用结局中甲方拿到的纸币面值表示终格局的价值。</p>
<p>下面考虑倒数第二层节点,在这些节点上,轮到我方选择,所以我们应该引入可选择的最大价值格局,因此每个节点的价值为其子节点的最大值:</p>
<p class="picture"><img alt="" src="/uploads/pictures/2048-ai-analysis/02.png"/></p>
<p>这些轮到我方的节点叫做max节点,max节点的值是其子节点最大值。</p>
<p>倒数第三层轮到对方选择,假设对方会尽力将局势引入让我方价值最小的格局,因此这些节点的价值取决于子节点的最小值。这些轮到对方的节点叫做min节点。</p>
<p>最后,根节点是max节点,因此价值取决于叶子节点的最大值。最终完整赋值的格局树如下:</p>
<p class="picture"><img alt="" src="/uploads/pictures/2048-ai-analysis/03.png"/></p>
<p>总结一下Minimax算法的步骤:</p>
<ol>
<li>首先确定最大搜索深度D,D可能达到终局,也可能是一个中间格局。</li>
<li>在最大深度为D的格局树叶子节点上,使用预定义的价值评价函数对叶子节点价值进行评价。</li>
<li>自底向上为非叶子节点赋值。其中max节点取子节点最大值,min节点取子节点最小值。</li>
<li>每次轮到我方时(此时必处在格局树的某个max节点),选择价值等于此max节点价值的那个子节点路径。</li>
</ol>
<p>在上面的例子中,根节点的价值为20,表示如果对方每一步都完美决策,则我方按照上述算法可最终拿到20元,这是我方在Minimax算法下最好的决策。格局转换路径如下图红色路径所示:</p>
<p class="picture"><img alt="" src="/uploads/pictures/2048-ai-analysis/04.png"/></p>
<p>对于真实问题中的Minimax,再次强调几点:</p>
<ul>
<li>真实问题一般无法构造出完整的格局树,所以需要确定一个最大深度D,每次最多从当前格局向下计算D层。</li>
<li>因为上述原因,Minimax一般是寻找一个局部最优解而不是全局最优解,搜索深度越大越可能找到更好的解,但计算耗时会呈指数级膨胀。</li>
<li>也是因为无法一次构造出完整的格局树,所以真实问题中Minimax一般是边对弈边计算局部格局树,而不是只计算一次,但已计算的中间结果可以缓存。</li>
</ul>
<h2 id="alpha-beta剪枝">Alpha-beta剪枝</h2>
<p>简单的Minimax算法有一个很大的问题就是计算复杂性。由于所需搜索的节点数随最大深度呈指数膨胀,而算法的效果往往和深度相关,因此这极大限制了算法的效果。</p>
<p>Alpha-beta剪枝是对Minimax的补充和改进。采用Alpha-beta剪枝后,我们可不必构造和搜索最大深度D内的所有节点,在构造过程中,如果发现当前格局再往下不能找到更好的解,我们就停止在这个格局及以下的搜索,也就是剪枝。</p>
<p>Alpha-beta基于这样一种朴素的思想:时时刻刻记得当前已经知道的最好选择,如果从当前格局搜索下去,不可能找到比已知最优解更好的解,则停止这个格局分支的搜索(剪枝),回溯到父节点继续搜索。</p>
<p>Alpha-beta算法可以看成变种的Minimax,基本方法是从根节点开始采用深度优先的方式构造格局树,在构造每个节点时,都会读取此节点的alpha和beta两个值,其中alpha表示搜索到当前节点时已知的最好选择的下界,而beta表示从这个节点往下搜索最坏结局的上界。由于我们假设对手会将局势引入最坏结局之一,因此当beta小于alpha时,表示从此处开始不论最终结局是哪一个,其上限价值也要低于已知的最优解,也就是说已经不可能此处向下找到更好的解,所以就会剪枝。</p>
<p>下面同样以上述示例介绍Alpha-beta剪枝算法的工作原理。我们从根节点开始,详述使用Alpha-beta的每一个步骤:</p>
<ol>
<li>根节点的alpha和beta分别被初始化为\(-\infty\),和\(+\infty\)。</li>
<li>深度优先搜索第一个孩子,不是叶子节点,所以alpha和beta继承自父节点,分别为\(-\infty\),和\(+\infty\)</li>
<li>搜索第三层的第一个孩子,同上。</li>
<li><p>搜索第四层,到达叶子节点,采用评价函数得到此节点的评价值为1。</p>
<p class="picture"><img alt="" src="/uploads/pictures/2048-ai-analysis/05.png"/></p>
</li>
<li><p>此叶节点的父节点为max节点,因此更新其alpha值为1,表示此节点取值的下界为1。</p>
</li>
<li>再看另外一个子节点,值为20,大于当前alpha值,因此将alpha值更新为20。</li>
<li>此时第三层最左节点所有子树搜索完毕,作为max节点,更新其真实值为当前alpha值:20。</li>
<li><p>由于其父节点(第二层最左节点)为min节点,因此更新其父节点beta值为20,表示这个节点取值最多为20。</p>
<p class="picture"><img alt="" src="/uploads/pictures/2048-ai-analysis/06.png"/></p>
</li>
<li><p>搜索第二层最左节点的第二个孩子及其子树,按上述逻辑,得到值为50(注意第二层最左节点的beta值要传递给孩子)。由于50大于20,不更新min节点的beta值。</p>
<p class="picture"><img alt="" src="/uploads/pictures/2048-ai-analysis/07.png"/></p>
</li>
<li><p>搜索第二层最左节点的第三个孩子。当看完第一个叶子节点后,发现第三个孩子的alpha=beta,此时表示这个节点下不会再有更好解,于是剪枝。</p>
<p class="picture"><img alt="" src="/uploads/pictures/2048-ai-analysis/08.png"/></p>
</li>
<li><p>继续搜索B分支,当搜索完B分支的第一个孩子后,发现此时B分支的alpha为20,beta为10。这表示B分支节点的最大取值不会超过10,而我们已经在A分支取到20,此时满足alpha大于等于beta的剪枝条件,因此将B剪枝。并将B分支的节点值设为10,注意,这个10不一定是这个节点的真实值,而只是上线,B节点的真实值可能是5,可能是1,可能是任何小于10的值。但是已经无所谓了,反正我们知道这个分支不会好过A分支,因此可以放弃了。</p>
<p class="picture"><img alt="" src="/uploads/pictures/2048-ai-analysis/09.png"/></p>
</li>
<li><p>在C分支搜索时遇到了与B分支相同的情况。因此讲C分支剪枝。</p>
<p class="picture"><img alt="" src="/uploads/pictures/2048-ai-analysis/10.png"/></p>
</li>
</ol>
<p>此时搜索全部完毕,而我们也得到了这一步的策略:应该走A分支。</p>
<p>可以看到相比普通Minimax要搜索18个叶子节点相比,这里只搜索了9个。采用Alpha-beta剪枝,可以在相同时间内加大Minimax的搜索深度,因此可以获得更好的效果。并且Alpha-beta的解和普通Minimax的解是一致的。</p>
<h1 id="针对2048游戏的实现">针对2048游戏的实现</h1>
<p>下面看一下ov3y同学针对2048实现的AI。程序的github在<a href="https://github.com/ov3y/2048-AI">这里</a>,主要程序都在<a href="https://github.com/ov3y/2048-AI/blob/master/js/ai.js">ai.js</a>中。</p>
<h2 id="建模">建模</h2>
<p>上面说过Minimax和Alpha-beta都是针对信息对称的轮流对弈问题,这里作者是这样抽象游戏的:</p>
<ul>
<li>我方:游戏玩家。每次可以选择上、下、左、右四个行棋策略中的一种(某些格局会少于四种,因为有些方向不可走)。行棋后方块按照既定逻辑移动及合并,格局转换完成。</li>
<li>对方:计算机。在当前任意空格子里放置一个方块,方块的数值可以是2或4。放置新方块后,格局转换完成。</li>
<li>胜利条件:出现某个方块的数值为“2048”。</li>
<li>失败条件:格子全满,且无法向四个方向中任何一个方向移动(均不能触发合并)。</li>
</ul>
<p>如此2048游戏就被建模成一个信息对称的双人对弈问题。</p>
<h2 id="格局评价">格局评价</h2>
<p>作为算法的核心,如何评价当前格局的价值是重中之重。在2048中,除了终局外,中间格局并无非常明显的价值评价指标,因此需要用一些启发式的指标来评价格局。那些分数高的“好”格局是容易引向胜利的格局,而分低的“坏”格局是容易引向失败的格局。</p>
<p>作者采用了如下几个启发式指标。</p>
<h3 id="单调性">单调性</h3>
<p>单调性指方块从左到右、从上到下均遵从递增或递减。一般来说,越单调的格局越好。下面是一个具有良好单调格局的例子:</p>
<p class="picture"><img alt="" src="/uploads/pictures/2048-ai-analysis/11.png"/></p>
<h3 id="平滑性">平滑性</h3>
<p>平滑性是指每个方块与其直接相邻方块数值的差,其中差越小越平滑。例如2旁边是4就比2旁边是128平滑。一般认为越平滑的格局越好。下面是一个具有极端平滑性的例子:</p>
<p class="picture"><img alt="" src="/uploads/pictures/2048-ai-analysis/12.png"/></p>
<h3 id="空格数">空格数</h3>
<p>这个很好理解,因为一般来说,空格子越少对玩家越不利。所以我们认为空格越多的格局越好。</p>
<h3 id="孤立空格数">孤立空格数</h3>
<p>这个指标评价空格被分开的程度,空格越分散则格局越差。</p>
<p>具体来说,2048-AI在评价格局时,对这些启发指标采用了加权策略。具体代码如下:</p>
<pre class="prettyprint linenums lang-js">// static evaluation function
AI.prototype.eval = function() {
var emptyCells = this.grid.availableCells().length;
var smoothWeight = 0.1,
//monoWeight = 0.0,
//islandWeight = 0.0,
mono2Weight = 1.0,
emptyWeight = 2.7,
maxWeight = 1.0;
return this.grid.smoothness() * smoothWeight
//+ this.grid.monotonicity() * monoWeight
//- this.grid.islands() * islandWeight
+ this.grid.monotonicity2() * mono2Weight
+ Math.log(emptyCells) * emptyWeight
+ this.grid.maxValue() * maxWeight;
};
</pre>
<p>有兴趣的同学可以调整一下权重看看有什么效果。</p>
<h2 id="对对方选择的剪枝">对对方选择的剪枝</h2>
<p>在这个程序中,除了采用Alpha-beta剪枝外,在min节点还采用了另一种剪枝,即只考虑对方走出让格局最差的那一步(而实际2048中计算机的选择是随机的),而不是搜索全部对方可能的走法。这是因为对方所有可能的选择为“空格数×2”,如果全部搜索的话会严重限制搜索深度。</p>
<p>相关剪枝代码如下:</p>
<pre class="prettyprint linenums lang-js">// try a 2 and 4 in each cell and measure how annoying it is
// with metrics from eval
var candidates = [];
var cells = this.grid.availableCells();
var scores = { 2: [], 4: [] };
for (var value in scores) {
for (var i in cells) {
scores[value].push(null);
var cell = cells[i];
var tile = new Tile(cell, parseInt(value, 10));
this.grid.insertTile(tile);
scores[value][i] = -this.grid.smoothness() + this.grid.islands();
this.grid.removeTile(cell);
}
}
// now just pick out the most annoying moves
var maxScore = Math.max(Math.max.apply(null, scores[2]), Math.max.apply(null, scores[4]));
for (var value in scores) { // 2 and 4
for (var i=0; i<scores[value].length; i++) {
if (scores[value][i] == maxScore) {
candidates.push( { position: cells[i], value: parseInt(value, 10) } );
}
}
}
</pre>
<h2 id="搜索深度">搜索深度</h2>
<p>在2048-AI的实现中,并没有限制搜索的最大深度,而是限制每次“思考”的时间。这里设定了一个超时时间,默认为100ms,在这个时间内,会从1开始,搜索到所能达到的深度。相关代码:</p>
<pre class="prettyprint linenums lang-js">// performs iterative deepening over the alpha-beta search
AI.prototype.iterativeDeep = function() {
var start = (new Date()).getTime();
var depth = 0;
var best;
do {
var newBest = this.search(depth, -10000, 10000, 0 ,0);
if (newBest.move == -1) {
//console.log('BREAKING EARLY');
break;
} else {
best = newBest;
}
depth++;
} while ( (new Date()).getTime() - start < minSearchTime);
//console.log('depth', --depth);
//console.log(this.translate(best.move));
//console.log(best);
return best
}
</pre>
<p>因此这个算法实现的效果实际上依赖于执行javascript引擎机器的性能。当然可以通过增加超时时间来达到更好的效果,但此时每一步行走速度会相应变慢。</p>
<h2 id="算法的改进">算法的改进</h2>
<p>目前这个实现作者声称成功合成2048的概率超过90%,但是合成4096甚至8192的概率并不高。作者在<a href="https://github.com/ov3y/2048-AI/blob/master/README.md">github项目的REAMDE</a>中同时给出了一些优化建议,这些建议包括:</p>
<ul>
<li>缓存结果。目前这个实现并没有对已搜索的树做缓存,每一步都要重新开始搜索。</li>
<li>多线程搜索。由于javascript引擎的单线程特性,这一点很难做到,但如果在其它平台上也许也可考虑并行技术。</li>
<li>更好的启发函数。也许可以总结出一些更好的启发函数来评价格局价值。</li>
</ul>
<h1 id="参考文献">参考文献</h1>
<ol>
<li><a href="http://gabrielecirulli.github.io/2048/">2048 Game</a></li>
<li><a href="https://github.com/ov3y/2048-AI">2048-AI github</a></li>
<li><a href="http://www.flyingmachinestudios.com/programming/minimax/">An Exhaustive Explanation of Minimax, a Staple AI Algorithm</a></li>
<li><a href="http://www.neverstopbuilding.com/minimax">Tic Tac Toe: Understanding the Minimax Algorithm</a></li>
<li><a href="http://cs.ucla.edu/~rosen/161/notes/alphabeta.html">CS 161 Recitation Notes - Minimax with Alpha Beta Pruning</a></li>
</ol>
</description>
</item>
<item>
<title>抓取网页内容生成Kindle电子书</title>
<link>http://blog.codinglabs.org/articles/convert-html-to-kindle-book.html?utm_source=rss&utm_medium=rss&utm_campaign=rss</link>
<guid>http://blog.codinglabs.org/articles/convert-html-to-kindle-book.html</guid>
<author>[email protected] 张洋</author>
<pubDate>Thu, 27 Mar 2014 00:00:00 +0800</pubDate>
<description><p>自从买了kindle后,总是想着如何最大效用发挥其效用。虽然多看上有很多书可以购买,网上也有很多免费的电子书,但是仍然有很多感兴趣的内容是以网页的形式存在的。例如<a href="http://chimera.labs.oreilly.com/books/">O’Reilly Atlas</a>就提供了诸多电子书,但是只提供免费的在线阅读;另外还有很多资料或文档都只有网页形式。于是就希望通过某种方法讲这些在线资料转为epub或mobi格式,以便在kindle上阅读。这篇文章介绍了如何借助calibre并编写少量代码来达到这个目的。</p>
<h1 id="calibre">Calibre</h1>
<h2 id="calibre简介">Calibre简介</h2>
<p><a href="http://calibre-ebook.com/">Calibre</a>是一个免费的电子书管理工具,可以兼容Windows, OS X及Linux,令人欣喜的是,除了GUI外,calibre还提供了诸多命令行工具,其中的ebook-convert命令可以根据用户编写的recipes文件(实际是python代码)抓取指定页面内容并生成mobi等格式的电子书。通过编写recipes可以自定制抓取行为,以适应不同的网页结构。</p>
<h2 id="安装calibre">安装Calibre</h2>
<p>Calibre的下载地址是<a href="http://calibre-ebook.com/download">http://calibre-ebook.com/download</a>,可以根据自己的操作系统下载相应的安装程序。</p>
<p>如果是Linux操作系统,还可以通过软件仓库安装:</p>
<p>Archlinux:</p>
<pre class="prettyprint linenums lang-bash">pacman -S calibre
</pre>
<p>Debian/Ubuntu:</p>
<pre class="prettyprint linenums lang-bash">apt-get install calibre
</pre>
<p>RedHat/Fedora/CentOS:</p>
<pre class="prettyprint linenums lang-bash">yum -y install calibre
</pre>
<p>注意,如果你使用OSX,需要单独安装<a href="http://manual.calibre-ebook.com/cli/cli-index.html">Command Line Tool</a>。</p>
<h1 id="抓取网页生成电子书">抓取网页生成电子书</h1>
<p>下面以<a href="http://chimera.labs.oreilly.com/books/1230000000561">Git Pocket Guide</a>为例,说明如何通过calibre从网页生成电子书。</p>
<h2 id="找到index页">找到index页</h2>
<p>要抓取整本书,第一件事就是找到index页,这个页面一般是Table of Contents,也就是目录页,其中每个目录项链接到相应内容页。index页将会指导抓取哪些页面以及生成电子书时内容组织顺序。在这个例子中,index页面是<a href="http://chimera.labs.oreilly.com/books/1230000000561/index.html">http://chimera.labs.oreilly.com/books/1230000000561/index.html</a>。</p>
<h2 id="编写recipes">编写recipes</h2>
<p>Recipes是一个以recipe为扩展名的脚本,内容实际上是一段python代码,用来定义calibre抓取页面的范围和行为,下面是用于抓取Git Pocket Guide的recipes:</p>
<pre class="prettyprint linenums lang-python">from calibre.web.feeds.recipes import BasicNewsRecipe
class Git_Pocket_Guide(BasicNewsRecipe):
title = 'Git Pocket Guide'
description = ''
cover_url = 'http://akamaicovers.oreilly.com/images/0636920024972/lrg.jpg'
url_prefix = 'http://chimera.labs.oreilly.com/books/1230000000561/'
no_stylesheets = True
keep_only_tags = [{ 'class': 'chapter' }]
def get_title(self, link):
return link.contents[0].strip()
def parse_index(self):
soup = self.index_to_soup(self.url_prefix + 'index.html')
div = soup.find('div', { 'class': 'toc' })
articles = []
for link in div.findAll('a'):
if '#' in link['href']:
continue
if not 'ch' in link['href']:
continue
til = self.get_title(link)
url = self.url_prefix + link['href']
a = { 'title': til, 'url': url }
articles.append(a)
ans = [('Git_Pocket_Guide', articles)]
return ans
</pre>
<p>下面分别解释代码中不同部分。</p>
<h3 id="总体结构">总体结构</h3>
<p>总体来看,一个recipes就是一个python class,只不过这个class必须继承calibre.web.feeds.recipes.BasicNewsRecipe。</p>
<h3 id="parse_index">parse_index</h3>
<p>整个recipes的核心方法是parse_index,也是recipes唯一必须实现的方法。这个方法的目标是通过分析index页面的内容,返回一个稍显复杂的数据结构(稍后介绍),这个数据结构定义了整个电子书的内容及内容组织顺序。</p>
<h3 id="总体属性设置">总体属性设置</h3>
<p>在class的开始,定义了一些全局属性:</p>
<pre class="prettyprint linenums lang-python">title = 'Git Pocket Guide'
description = ''
cover_url = 'http://akamaicovers.oreilly.com/images/0636920024972/lrg.jpg'
url_prefix = 'http://chimera.labs.oreilly.com/books/1230000000561/'
no_stylesheets = True
keep_only_tags = [{ 'class': 'chapter' }]
</pre>
<ul>
<li>title:电子书标题</li>
<li>description:电子书描述</li>
<li>cover_url:电子书的封面图片</li>
<li>url_prefix:这是我自用的属性,是内容页面的前缀,用于后面拼装内容页的完整url</li>
<li>no_stylesheets:不要使用页面CSS样式</li>
<li>keep_only_tags:这一行告诉calibre分析index页时仅考虑class属性为“chapter”的DOM元素,如果你看index页的源码会发现这对应一级标题。之所以这样是因为在这个例子中,index页面每个一级标题对应一个独立内容页,而二级标题仅链接到页面中某个锚点(anchor),所以仅需考虑一级标题</li>
</ul>
<h3 id="parse_index返回值">parse_index返回值</h3>
<p>下面介绍parse_index需要通过分析index页面返回的数据结构。</p>
<p class="picture"><img alt="" src="/uploads/pictures/convert-html-to-kindle-book/01.png"/></p>
<p>总体返回数据结构是一个list,其中每个元素是一个tuple,一个tuple表示一卷(volume)。在这个例子中只有一卷,所以list中只有一个tuple。</p>
<p>每个tuple有两个元素,第一个元素是卷名,第二个元素是一个list,list中每个元素是一个map,表示一章(chapter),map中有两个元素:title和url,title是章节标题,url是章节所在内容页的url。</p>
<p>Calibre会根据parse_index的返回结果抓取并组织整个书,并且会自行抓取并处理内容中外链的图片。</p>
<p>整个parse_index使用soup解析index页并生成上述数据结构。</p>
<h3 id="更多">更多</h3>
<p>上面是最基本的recipes,想了解更多的使用方法,可以参考<a href="http://manual.calibre-ebook.com/news_recipe.html">API文档</a>。</p>
<h2 id="生成mobi">生成mobi</h2>
<p>编写好recipes后,在命令行下通过如下命令即可生成电子书:</p>
<pre class="prettyprint linenums lang-bash">ebook-convert Git_Pocket_Guide.recipe Git_Pocket_Guide.mobi
</pre>
<p>即可生成mobi格式的电子书。ebook-convert会根据recipes代码自行抓取相关内容并组织结构。</p>
<h1 id="最终效果">最终效果</h1>
<p>下面是在kindle上看到的效果。</p>
<p>目录</p>
<p class="picture"><img alt="" src="/uploads/pictures/convert-html-to-kindle-book/02.jpg"/></p>
<p>内容一</p>
<p class="picture"><img alt="" src="/uploads/pictures/convert-html-to-kindle-book/03.jpg"/></p>
<p>内容二</p>
<p class="picture"><img alt="" src="/uploads/pictures/convert-html-to-kindle-book/04.jpg"/></p>
<p>含有图片的页</p>
<p class="picture"><img alt="" src="/uploads/pictures/convert-html-to-kindle-book/05.jpg"/></p>
<p>实际效果</p>
<p class="picture"><img alt="" src="/uploads/pictures/convert-html-to-kindle-book/06.jpg"/></p>
<h1 id="我的recipes仓库">我的recipes仓库</h1>
<p>我在github上建了一个<a href="https://github.com/ericzhang-cn/kindle-open-books/">kindle-open-books</a>,里面放了一些recipes,有我写的,也有其他同学贡献的。欢迎任何人贡献的recipes。</p>
</description>
</item>
<item>
<title>开始使用Ubuntu作为工作环境</title>
<link>http://blog.codinglabs.org/articles/getting-started-with-ubuntu.html?utm_source=rss&utm_medium=rss&utm_campaign=rss</link>
<guid>http://blog.codinglabs.org/articles/getting-started-with-ubuntu.html</guid>
<author>[email protected] 张洋</author>
<pubDate>Mon, 30 Dec 2013 00:00:00 +0800</pubDate>
<description><p>2012年3月,我自购了一台13寸的Macbook Air,从那时开始至今近两年时间,我一直用它作为工作本。但是最近越来越觉得4G的内存和128G的SSD力不从心,苦于Air无法升级硬件,于是终于下决心拿出入职时公司给配的Dell E6410,自己买了内存和SSD,升级成了8G内存+370G混合硬盘(120G SSD做主盘,250G硬盘做从盘)。</p>
<p>硬件升级事小,关键是系统的迁移代价比较大。我在Dell本上装的是Ubuntu 13.10,由于我平时习惯使用Dropbox等云端服务,浏览器配置也都通过Google账号漫游,所以这部分迁移几乎没有成本,主要的成本在开发环境配置和常用软件迁移。虽然都是Unix系,但是Mac OSX下很多软件Linux下并没有。</p>
<p>花了大约一个周末,总算把我的Ubuntu配置的比较顺手了,目前也已经正式投入工作。其中的重头戏便是开发环境(主要是terminal和vim)的配置,另外就是一些常用工具。这篇文章记录了我一些主要的工作,算是给自己留一个文档,也希望能给打算从Mac迁移到Linux的同学做一个借鉴。</p>
<h1 id="开发环境配置">开发环境配置</h1>
<p>之前在Mac下,我直接使用的是<a href="https://github.com/square/maximum-awesome">maximum-awesome</a>,开发环境这块完全不用自己操心。可惜maximum-awesome只能在Mac下使用,并没有Linux版。于是需要自己做一些工作,以便让开发环境够顺手。</p>
<h2 id="使用terminator作为终端">使用terminator作为终端</h2>
<h3 id="安装terminator">安装terminator</h3>
<p>Ubuntu自带的终端是gnome-terminal,虽然也还不错,但是不能支持屏幕分割、选择复制等功能让我很不爽,于是我换用terminator作为终端,terminator可以支持屏幕分割,并且默认快捷键和gnome-terminal无异,熟悉gnome-terminal的话可以快速上手。</p>
<p>Ubuntu下可以这样安装terminator:</p>
<pre class="prettyprint linenums lang-bash">sudo apt-get install terminator
</pre>
<h3 id="terminator常用快捷键">terminator常用快捷键</h3>
<ul>
<li>Ctrl-Shift-c 拷贝</li>
<li>Ctrl-Shift-v 粘贴</li>
<li>Ctrl-Shift-t 开新Tab窗口</li>
<li>Ctrl-Shift-o 上下拆分屏幕</li>
<li>Ctrl-Shift-e 左右拆分屏幕</li>
<li>Ctrl-Shift-w 关闭当前窗口</li>
<li>Ctrl-Shift-q 关闭整个终端</li>
</ul>
<h2 id="配置terminator使用solarized配色">配置terminator使用solarized配色</h2>
<h3 id="使用terminator-solarized">使用terminator-solarized</h3>
<p>maximum-awesome所使用的<a href="http://ethanschoonover.com/solarized">solarized</a>配色是相当不错的,所以自然希望继续使用。针对terminator的solarized配色已经有人专门做好了:<a href="https://github.com/ghuntley/terminator-solarized">terminator-solarized</a>,只要按如下操作就可以使用:</p>
<pre class="prettyprint linenums lang-bash">mkdir -p ~/.config/terminator/
curl https://raw.github.com/ghuntley/terminator-solarized/master/config > ~/.config/terminator/config
</pre>
<p>然后重新打开terminator就已经是solarized配色了。</p>
<h3 id="对terminator更多的配置">对terminator更多的配置</h3>
<p>接下来,可以在terminator-solarized配置文件的基础上进行更多的配置,例如背景透明、启用选择复制等。</p>
<p>关于terminator的详细配置选项可以参考<a href="http://manpages.ubuntu.com/manpages/intrepid/man5/terminator_config.5.html">terminator manpage</a>,下面贴出我的~/.config/terminator/config供参考:</p>
<pre class="prettyprint linenums lang-bash">[global_config]
title_transmit_bg_color = "#d30102"
focus = system
suppress_multiple_term_dialog = True
[keybindings]
[profiles]
[[default]]
palette = "#073642:#dc322f:#859900:#b58900:#268bd2:#d33682:#2aa198:#eee8d5:#002b36:#cb4b16:#586e75:#657b83:#839496:#6c71c4:#93a1a1:#fdf6e3"
copy_on_selection = True
background_image = None
background_darkness = 0.95
background_type = transparent
use_system_font = False
cursor_color = "#eee8d5"
foreground_color = "#839496"
show_titlebar = False
font = Monospace 11
background_color = "#002b36"
[[solarized-dark]]
palette = "#073642:#dc322f:#859900:#b58900:#268bd2:#d33682:#2aa198:#eee8d5:#002b36:#cb4b16:#586e75:#657b83:#839496:#6c71c4:#93a1a1:#fdf6e3"
background_color = "#002b36"
background_image = None
cursor_color = "#eee8d5"
foreground_color = "#839496"
[[solarized-light]]
palette = "#073642:#dc322f:#859900:#b58900:#268bd2:#d33682:#2aa198:#eee8d5:#002b36:#cb4b16:#586e75:#657b83:#839496:#6c71c4:#93a1a1:#fdf6e3"
background_color = "#fdf6e3"
background_image = None
cursor_color = "#002b36"
foreground_color = "#657b83"
[layouts]
[[default]]
[[[child1]]]
type = Terminal
parent = window0
profile = default
[[[window0]]]
type = Window
parent = ""
[plugins]
</pre>
<h3 id="配置dircolors">配置dircolors</h3>
<p>完成上述配置后,你会发现用ls命令查看目录和文件时是一片灰色。这是因为默认情况下solarized各种bright方案基本都是灰色,而系统默认显示目录和文件时多用bright色,此时需要配置dircolors才能显示出彩色的文件和目录。</p>
<p><a href="https://github.com/seebi/dircolors-solarized">dircolors-solarized</a>项目提供了适合于solarized的dircolors配色方案,只要选择合适的方案使用就可以了。例如我是用的solarized dark配色,所以可以选择适合这个配色的dircolors.ansi-dark:</p>
<pre class="prettyprint linenums lang-bash">curl https://raw.github.com/seebi/dircolors-solarized/master/dircolors.ansi-dark > ~/.dircolors
</pre>
<p>然后在~/.bashrc中加入如下配置:</p>
<pre class="prettyprint linenums lang-bash"># enable color support of ls and also add handy aliases
if [ -x /usr/bin/dircolors ]; then
test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)"
alias ls='ls --color=auto'
#alias dir='dir --color=auto'
#alias vdir='vdir --color=auto'
alias grep='grep --color=auto'
alias fgrep='fgrep --color=auto'
alias egrep='egrep --color=auto'
fi
# some more ls aliases
alias ll='ls -alF'
alias la='ls -A'
alias l='ls -CF'
</pre>
<p>执行</p>
<pre class="prettyprint linenums lang-bash">source ~/.bashrc
</pre>
<p>后,再执行ls或ll就可以看到彩色的目录或文件了。</p>
<p>配置完的terminator效果如下:</p>
<p class="picture"><img alt="" src="/uploads/pictures/getting-started-with-ubuntu/01.png"/></p>
<h2 id="配置vim">配置VIM</h2>
<p>vim作为我日常使用频率最高的代码编辑器,自然要好好配置一番。之前的maximum-awesome自带了很多vim插件,这里我没有精力完全按照maximum-awesome的插件配置,只选取了日常比较常用的几个插件先配上,后面需要的话可以再加。</p>
<h3 id="插件">插件</h3>
<p>我目前安装的插件有:</p>
<ul>
<li><a href="https://github.com/scrooloose/nerdtree">NERDTree</a>:可以在单独的window中浏览目录和文件,方便打开的选取文件。</li>
<li><a href="https://github.com/vim-scripts/taglist.vim">taglist</a>:可以通过ctags生成的tag文件索引定位代码中的常量、函数、类等结构,阅读代码和写代码必备。</li>
<li><a href="https://github.com/Lokaltog/vim-powerline">powerline</a>:在底部显示一个非常漂亮的状态条,还可以通过不同的颜色提醒用户当前处于什么状态(如normal、insert或visual)。</li>
<li><a href="https://github.com/altercation/vim-colors-solarized">vim-colors-solarized</a>:vim的solarized配色插件。</li>
</ul>
<p>如果所有插件都按vim标准方法安装,各种插件会非常分散,不便于管理,于是我选用<a href="https://github.com/tpope/vim-pathogen">pathogen</a>安装和管理vim插件。pathogen允许将各个插件放在.vim/bundle/下各自的目录中,通过启动时自动加载所有插件。</p>
<h3 id="自动配置工具">自动配置工具</h3>
<p>整个配置过程过于繁琐不再赘述,我已经将配置过程写成了一个自动配置脚本并放到了github:<a href="https://github.com/ericzhang-cn/vim-conf">https://github.com/ericzhang-cn/vim-conf</a>,需要的朋友只要clone下来并运行init.sh脚本就可以自动完成整个配置:</p>
<pre class="prettyprint linenums lang-bash">git clone https://github.com/ericzhang-cn/vim-conf.git
cd vim-conf && ./init.sh
</pre>
<p>最终配置效果如下:</p>
<p class="picture"><img alt="" src="/uploads/pictures/getting-started-with-ubuntu/02.png"/></p>
<p>(更新:当前我已经换用<a href="https://github.com/ericzhang-cn/maximum-awesome-linux">maximum-awesome-linux</a>,不再维护之前那个配置脚本)</p>
<h3 id="快捷键">快捷键</h3>
<p>其中并没有对vim默认的快捷键做过多重设,只有两个:</p>
<ul>
<li>,-d:打开或关闭NERDTree</li>
<li>,-t:打开或关闭taglist</li>
</ul>
<p>(更新:换用maximum-awesome-linux后快捷键会不一样,具体请参考<a href="https://github.com/ericzhang-cn/maximum-awesome-linux/blob/master/README.md">README</a>)</p>
<h1 id="常用工具">常用工具</h1>
<h2 id="浏览器">浏览器</h2>
<p>Ubuntu自带的是Firefox,我平常使用的是Chromium,这点在Ubuntu下没任何问题,可以直接安装:</p>
<pre class="prettyprint linenums lang-bash">sudo apt-get install chromium-browser
</pre>
<p>用Google账号登录后,书签、插件等会自动同步,非常方便。</p>
<h2 id="搜狗输入法-amp-谷歌输入法">搜狗输入法&谷歌输入法</h2>
<p>Mac下有搜狗输入法或百度输入法。不过目前搜狗也基于fcitx做了linux版的搜狗输入法。</p>
<p>Ubuntu自带的ibus直接卸掉,然后安装<a href="https://fcitx-im.org/wiki/Fcitx">fcitx</a>:</p>
<pre class="prettyprint linenums lang-bash">sudo add-apt-repository ppa:fcitx-team/nightly && sudo apt-get update
sudo apt-get install fcitx-sogoupinyin
</pre>