-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.html
2134 lines (1704 loc) · 335 KB
/
index.html
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
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<meta name="theme-color" content="#222"><meta name="generator" content="Hexo 6.3.0">
<link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon-next.png">
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32-next.png">
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16-next.png">
<link rel="mask-icon" href="/images/logo.svg" color="#222">
<meta name="yandex-verification" content="3ac9ae36ddebb425">
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha256-HtsXJanqjKTc8vVQjO4YMhiqFoXkfBsjBWcX91T1jr8=" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.css" integrity="sha256-Vzbj7sDDS/woiFS3uNKo8eIuni59rjyNGtXfstRzStA=" crossorigin="anonymous">
<script class="next-config" data-name="main" type="application/json">{"hostname":"liupzmin.com","root":"/","images":"/images","scheme":"Gemini","darkmode":false,"version":"8.15.1","exturl":true,"sidebar":{"position":"left","display":"post","padding":18,"offset":12,"onmobile":true},"copycode":{"enable":false,"style":null},"bookmark":{"enable":false,"color":"#222","save":"auto"},"mediumzoom":false,"lazyload":true,"pangu":false,"comments":{"style":"tabs","active":null,"storage":true,"lazyload":false,"nav":null},"stickytabs":false,"motion":{"enable":false,"async":true,"transition":{"menu_item":"fadeInDown","post_block":"fadeIn","post_header":"slideDownIn","post_body":"slideDownIn","coll_header":"slideLeftIn","sidebar":"slideUpIn"}},"prism":false,"i18n":{"placeholder":"搜索...","empty":"没有找到任何搜索结果:${query}","hits_time":"找到 ${hits} 个搜索结果(用时 ${time} 毫秒)","hits":"找到 ${hits} 个搜索结果"}}</script><script src="/js/config.js"></script>
<meta name="description" content="左手人文 | 右手科技">
<meta property="og:type" content="website">
<meta property="og:title" content="兔子先生">
<meta property="og:url" content="http://liupzmin.com/index.html">
<meta property="og:site_name" content="兔子先生">
<meta property="og:description" content="左手人文 | 右手科技">
<meta property="og:locale" content="zh_CN">
<meta property="article:author" content="巴流">
<meta name="twitter:card" content="summary">
<link rel="canonical" href="http://liupzmin.com/">
<script class="next-config" data-name="page" type="application/json">{"sidebar":"","isHome":true,"isPost":false,"lang":"zh-CN","comments":"","permalink":"","path":"index.html","title":""}</script>
<script class="next-config" data-name="calendar" type="application/json">""</script>
<title>兔子先生 - 探寻计算机的历史与哲学密码</title>
<noscript>
<link rel="stylesheet" href="/css/noscript.css">
</noscript>
<link rel="alternate" href="/atom.xml" title="兔子先生" type="application/atom+xml">
</head>
<body itemscope itemtype="http://schema.org/WebPage">
<div class="headband"></div>
<main class="main">
<div class="column">
<header class="header" itemscope itemtype="http://schema.org/WPHeader"><div class="site-brand-container">
<div class="site-nav-toggle">
<div class="toggle" aria-label="切换导航栏" role="button">
<span class="toggle-line"></span>
<span class="toggle-line"></span>
<span class="toggle-line"></span>
</div>
</div>
<div class="site-meta">
<a href="/" class="brand" rel="start">
<i class="logo-line"></i>
<h1 class="site-title">兔子先生</h1>
<i class="logo-line"></i>
</a>
<p class="site-subtitle" itemprop="description">探寻计算机的历史与哲学密码</p>
</div>
<div class="site-nav-right">
<div class="toggle popup-trigger" aria-label="搜索" role="button">
</div>
</div>
</div>
<nav class="site-nav">
<ul class="main-menu menu"><li class="menu-item menu-item-home"><a href="/" rel="section"><i class="home fa-fw"></i>首页</a></li><li class="menu-item menu-item-archives"><a href="/archives/" rel="section"><i class="archive fa-fw"></i>归档<span class="badge">60</span></a></li><li class="menu-item menu-item-tags"><a href="/tags/" rel="section"><i class="tags fa-fw"></i>标签<span class="badge">63</span></a></li><li class="menu-item menu-item-categories"><a href="/categories/" rel="section"><i class="th fa-fw"></i>分类<span class="badge">32</span></a></li><li class="menu-item menu-item-about"><a href="/about/" rel="section"><i class="user fa-fw"></i>关于</a></li>
</ul>
</nav>
</header>
<aside class="sidebar">
<div class="sidebar-inner sidebar-overview-active">
<ul class="sidebar-nav">
<li class="sidebar-nav-toc">
文章目录
</li>
<li class="sidebar-nav-overview">
站点概览
</li>
</ul>
<div class="sidebar-panel-container">
<!--noindex-->
<div class="post-toc-wrap sidebar-panel">
</div>
<!--/noindex-->
<div class="site-overview-wrap sidebar-panel">
<div class="site-author animated" itemprop="author" itemscope itemtype="http://schema.org/Person">
<img class="site-author-image" itemprop="image" alt="巴流"
src="/images/gzh.jpg">
<p class="site-author-name" itemprop="name">巴流</p>
<div class="site-description" itemprop="description">左手人文 | 右手科技</div>
</div>
<div class="site-state-wrap animated">
<nav class="site-state">
<div class="site-state-item site-state-posts">
<a href="/archives/">
<span class="site-state-item-count">60</span>
<span class="site-state-item-name">日志</span>
</a>
</div>
<div class="site-state-item site-state-categories">
<a href="/categories/">
<span class="site-state-item-count">32</span>
<span class="site-state-item-name">分类</span></a>
</div>
<div class="site-state-item site-state-tags">
<a href="/tags/">
<span class="site-state-item-count">63</span>
<span class="site-state-item-name">标签</span></a>
</div>
</nav>
</div>
<div class="links-of-author animated">
<span class="links-of-author-item">
<span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL2xpdXB6bWlu" title="GitHub → https://github.com/liupzmin"><i class="github fa-fw"></i></span>
</span>
<span class="links-of-author-item">
<span class="exturl" data-url="bWFpbHRvOmxpdXB6bWluQGdtYWlsLmNvbQ==" title="E-Mail → mailto:[email protected]"><i class="envelope fa-fw"></i></span>
</span>
<span class="links-of-author-item">
<a href="/atom.xml" title="RSS → /atom.xml" rel="noopener me"><i class="fa fa-rss fa-fw"></i></a>
</span>
</div>
</div>
</div>
</div>
</aside>
</div>
<div class="main-inner index posts-expand">
<div class="post-block">
<article itemscope itemtype="http://schema.org/Article" class="post-content" lang="">
<link itemprop="mainEntityOfPage" href="http://liupzmin.com/2024/07/27/essay/rabbit-and-conjuror/">
<span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
<meta itemprop="image" content="/images/gzh.jpg">
<meta itemprop="name" content="巴流">
</span>
<span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
<meta itemprop="name" content="兔子先生">
<meta itemprop="description" content="左手人文 | 右手科技">
</span>
<span hidden itemprop="post" itemscope itemtype="http://schema.org/CreativeWork">
<meta itemprop="name" content="undefined | 兔子先生">
<meta itemprop="description" content="">
</span>
<header class="post-header">
<h2 class="post-title" itemprop="name headline">
<a href="/2024/07/27/essay/rabbit-and-conjuror/" class="post-title-link" itemprop="url">白兔与魔法师</a>
</h2>
<div class="post-meta-container">
<div class="post-meta">
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="far fa-calendar"></i>
</span>
<span class="post-meta-item-text">发表于</span>
<time title="创建时间:2024-07-27 10:17:59" itemprop="dateCreated datePublished" datetime="2024-07-27T10:17:59+08:00">2024-07-27</time>
</span>
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="far fa-calendar-check"></i>
</span>
<span class="post-meta-item-text">更新于</span>
<time title="修改时间:2024-08-02 12:16:16" itemprop="dateModified" datetime="2024-08-02T12:16:16+08:00">2024-08-02</time>
</span>
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="far fa-folder"></i>
</span>
<span class="post-meta-item-text">分类于</span>
<span itemprop="about" itemscope itemtype="http://schema.org/Thing">
<a href="/categories/%E9%9A%8F%E7%AC%94/" itemprop="url" rel="index"><span itemprop="name">随笔</span></a>
</span>
</span>
</div>
</div>
</header>
<div class="post-body" itemprop="articleBody">
<ol>
<li></li>
</ol>
<p>乔斯坦·贾德在他的哲学启蒙著作《苏菲的世界》中有过一个很形象的比喻:<strong>我们的世界就像是魔法师从礼帽中变出的一只白兔,白兔就像是整个宇宙,而我们人类则是寄居在兔子皮毛深处的微生物。而哲学家总是试图沿着兔子的细毛往上爬,想努力看清魔法师的脸。</strong></p>
<p>他说我们与小白兔之间唯一的不同是:小白兔并不明白它本身参与了一场魔术表演,我们则相反。我们觉得自己是某种神秘事物的一部分,我们想了解其中的奥秘。</p>
<p>两千多年前,一位古希腊哲学家认为,哲学之所以产生,是因为人有好奇心的缘故。当一切基本需求都满足之后,仍然还有些东西是每个人都需要的。“我们是谁?” “世界从何而来?” 没有一种文化不关心这样的问题。</p>
<p>书中用发人深省的文字如是写道:</p>
<blockquote>
<p>这世界就像魔术师从他的帽子里拉出的一只白兔。只是这白兔的体积极其庞大,因此这场戏法要数十亿年才变得出来。所有的生物都出生于这只兔子的细毛顶端,他们刚开始对于这场令人不可置信的戏法都感到惊奇。然而当他们年纪愈长,也就愈深入兔子的皮毛,并且待了下来。他们在那儿觉得非常安适,因此不愿意再冒险爬回脆弱的兔毛顶端。唯有哲学家才会踏上此一危险的旅程,迈向语言与存在所能达到的顶峰。其中有些人掉了下来,但也有些人死命攀住兔毛不放,并对那些窝在舒适柔软的兔毛深处、尽情吃喝的人们大声吼叫。</p>
</blockquote>
<p>《苏菲的世界》是写给青少年的哲学入门读物,但在三十多岁的我读来,满满都是自己的心路历程。白兔与魔法师的比喻真可以当作我思想嬗变的注脚,我人生的前三十年几乎可以说是空白的,直到如今才有了些许字迹。</p>
<p>大约是二十八岁的时候,我在思想上产生了不小的变化,这个过程并非一蹴而就,而是一个缓慢发酵的过程。要阐明我变成了怎样的人,就需要先说明我原本是怎样的人。</p>
<p>我是个木讷内向的人,寡言少语,兼具些许社恐。这其实不足为怪,因为很多人都是如此,但我最大的问题在于:太习惯这个世界!我对所经历的种种,没有一丝一毫的批判精神和思考精神,如果某件事第一次进入我的视线,我根本不会有任何怀疑倾向。</p>
<p>我对身边的一切天生具有一种漠然接受的态度,对任何事物都不感到好奇,对任何事物都没有自己的看法,别人如何做,我便如何做,全然不想为什么要这么做,别人告诉我是什么,我就当做什么,全然不思考为什么。像我这样的人,休谟看了肯定会大摇其头的。学生时代,我没想过一些书本之外的事,诸如某一历史人物、历史事件、某个概念等等,根本没有额外的想要去丰富课堂所学的想法,我对书上的知识,坦然受之。如今看一些东西,尤其涉及到旧有的知识点时,我很惊恐地发现,我就像刚刚认识它们一样。甚至亲身经历过的事,我好像也没有特殊的印象,比如香港回归,申奥成功等等,这些按理来说都是我亲历的时代大事件,却没有在我内心留下太多深刻的记忆。</p>
<p>我依然记得,一位挚友在回忆当年中国申奥成功的盛况时,曾这样写道:“当‘北京’两个字在萨马兰奇的口中说出时,我沸腾了,整个班级都沸腾了,我激动地将课本抛向空中,却不料它在落下时砸中了一位女同学的头……”</p>
<p>我很羡慕这种身临其境、历久弥新的回忆,这是我一直不曾有过的。我漠然接受的一切,多年之后,只丢给我一个模糊的背影,有一些甚至连背影都没有。</p>
<p>尤其糟糕的是,这种态度让我在待人接物方面表现得很难堪,有时候近乎冷漠。小的时候尚且可以用年幼无知的理由来搪塞,成年之后,那些儿时的借口便自然而然地失去了合理性,新手庇护期也就这样离我远去了。而我在为人处世方面很难做到有礼有节,在与人交谈中,有时候因思索过慢而错过了讲话的时机,索性也就不回答了;有时想好的话讲出来,却完全不是自己想要的效果;有时候对别人的发问,表现得如同条件反射一般,话一出口就懊悔不跌,仿佛那一瞬间原始的性格冲动占据着大脑,理性反而退居二线。我甚至觉得我与《局外人》的主人公默尔索有些相像,他的特点就是冷漠,对身边的一切漠不关心。这当然只是某种程度上的相似,事实上我只是不善于交际,且不会伪装自己,或者可以说,我是个真诚的人。而这必然会与周遭的环境格格不入,势必又会养成一份敏感的内心,所以我一直有一个附庸风雅、沽名钓誉的爱好:诗词!</p>
<p>我摘引两首我写过的诗词:</p>
<blockquote>
<p>应天长<br>时年细数,万颗红珍,抚叹华年流去。彼时秦门初遇,正盈盈细雨。算而今,载余过,更笑那、岁月无居。此犹记、当年游冶,不胜唏嘘。<br>长慨来日时,云烟飞絮,折煞黄金缕。忘于江湖路远,不忆神伤语。此一句,彼一句。听刘郎、笙歌几曲。且祝君、黄金榜上,杯酌饮去。</p>
<p>春归<br>繁花已尽无长期,绿欺山头几星移。<br>易解梅花寒放日,难忆少年苦读时。<br>道原忘食不得闲,老泉发愤犹未迟。<br>劝君须臾更惜取,莫道流水不能西。</p>
</blockquote>
<p>我那时对自己的大学专业完全提不起兴趣,只爱做这些悲春伤秋、感时伤世的句子,然而现在看来,都不过是贻笑大方而已,也许唬得住一两个人,但在稍具格律常识的人看来,全都是些不学无术之作。如果我那个时候能深入了解一下 100 年前的新文化运动,切身地体会一下当时的知识分子为推广白话的文学革命所做的种种努力,以及他们用现代文明的眼光对中国古代文化重新审视的历史,大概率我会改变崇古的文学趣味。毕竟鲁迅曾咬牙切齿地呐喊:“只要对于白话来加以谋害者,都应该灭亡!”</p>
<p>这大略就是我高中和大学时代兴趣爱好之所在,我实在耻于用“浸淫”这个词,因为当时虽有兴趣,却不曾真的从内心觉悟,并未意识到要为这兴趣做些刻苦的研究与努力,不曾为这兴趣多流些汗水,甚至连几本书也不曾多读。如今回首往事,青春也只是几篇为赋新词强说愁并且狗屁不通的诗词文章罢了!</p>
<ol start="2">
<li></li>
</ol>
<p>性格和兴趣爱好大抵如上所述,但这一切却从我二十八岁开始,渐渐地发生了翻天覆地的变化。其实性格已很难改变,主要是思想观念蜕变了,相应地兴趣爱好也为之一变。简言之,我对很多事物产生了哲学三问,“我是谁?我来自哪里?我将往何处去?” 我重新认识身边的一切,此后看事情便不同了。</p>
<p>这样说很抽象,我试举几个例子,就可见出我的变化了:</p>
<ul>
<li>《红楼梦》在我心目中的位置提高了。在四大名著的排行中,由最末位升到第一位。</li>
<li>我开始爱好哲学了。学生时代我是很讨厌政治和马克思哲学课的。</li>
<li>兴趣爱好从诗词歌赋,渐渐转移到历史、哲学、政治、社会学上去了。</li>
<li>开始喜欢中国近代史。确切一点说,是中国近代思想史,提起近代史,总是和屈辱联系在一起的,所以面对民族创伤,我总是敬而远之。但“西学东渐”引起了千年未有之大变局,中国的高级知识分子开始睁眼看世界,开始以新的标准尺度衡量中国传统文化思想,这是一个中西思想大碰撞的时代,其激烈程度是空前的。</li>
<li>开始对计算机专业产生了发自内心的热爱。</li>
</ul>
<p>二十八岁以前,要对一个涵咏在“骏马秋风冀北”与“杏花春雨江南”中的人说:<strong>哲学会成为你的爱好,政治、历史、社会学会成为你最爱的阅读领域</strong>。我相信他绝对会嗤之以鼻,并表示出绝对的自信与不屑。没错,我之所以说这是思想上的巨变,就是因为以前的我绝对不会承认现在的我。</p>
<p>那么,这种转变究竟何以开始的呢?</p>
<p>起因要追溯到我对“人生意义”的寻求上。这个原因看起来实在平平无奇,要说哪个青年没发出过对“人生的意义”的灵魂叩问,那么他绝对算不上是一个现代青年。上个世纪80年代的中国曾经掀起过一阵“萨特热”,当时的青年普遍关注“我为什么活着”、“人生的意义”这种特别令人迷惘的问题,裹挟着萨特哲学的西方文学也深深影响了80年代中国的文学青年。显然,追寻“人生的意义”并不是一件时髦的事儿,反而听上去有些陈词滥调。但是,这一次我没有停留在普通文青毫无意义的牢骚上,我竟然真的花心思去想了,原因完全是出于对时间和空间的无法释怀上,出于对人生既然相聚又难免分离的唏嘘不已上,这大部分要归因于我多愁善感的性格。</p>
<p>我少小离家,负笈于外地,工作后也远离故乡,天然就会对 “从此故乡只有冬夏,再无春秋” 这样的句子感同身受,也就很容易将思想引向“人生的意义”这种带有哲学意味的问题上去。要解答这个问题说来也容易,我们初中就接触到了其中一种答案,那是保尔柯察金在《钢铁是怎样练成的》一书中写就的著名段落:</p>
<blockquote>
<p>人最宝贵的是生命。生命对于每个人只有一次,人的一生应该这样度过:当回忆往事的时候,他不会因为虚度年华而悔恨,也不会因为碌碌无为而羞愧;在临死的时候,他能够说:“我的生命和全部精力都献给了世界上最壮丽的事业——为人类的解放事业而斗争。</p>
</blockquote>
<p>这是一种积极进取的人生态度,给生命赋予了无限的神圣使命,对苦难甘之如饴,我想这应该是每个人都能认同且可以奉为圭臬,拿来当做人生信条的。但答案并不只有这一种,答案可以有很多。如果拿这个问题去问胡适,他会说:“生命本没有意义,你要能给他什么意义,他就有什么意义。与其终日冥想人生有何意义,不如试用此生做点有意义的事。” 如果去问颜回,他会用行为给出君子忧道不忧贫的回答:“一箪食,一瓢饮,在陋巷,人不堪其忧,回也不改其乐。”如果去问犬儒哲学家第欧根尼,他会高呼:“像狗一样活着!”</p>
<p>我当时还想不到这么多,脑海里就只有保尔柯察金一个答案,但这个被塞进脑子里的答案是那么单薄,让我觉得这实在是来得太容易了。作为学生,我们都有过对老师和家长的谆谆教诲充耳不闻的经历,而最后真正能成为自己内心的知识,则或多或少都是自己冥思苦想得来的,或者是自己亲身体会到的,所谓“初听不识曲中意,再听已是曲中人”是也。孔子也说过“不愤不启,不悱不发”这样的话。也就是说,真正的智慧都来自内心,而保尔柯察金只是我少年时代被灌输进去的一种知识,让我诧异的是,成年后它居然还奇迹般地停留在我的脑海里。</p>
<p>后来,我看到一本叫做《一片叶子落下来》的儿童绘本,作者以树叶的视角向孩子们讲述生命和死亡的意义,其中最精彩的部分,我摘录在此:</p>
<blockquote>
<p>“我们死了会到哪儿去呢?”<br>“没有人知道,这是个大秘密!”<br>“春天的时候,我们会回来吗?”<br>“我们可能不会再回来了,但是生命会回来。”<br>“那么这一切有什么意思呢?” 弗雷迪继续问。<br>“如果我们反正是要掉落、死亡,那为什么还要来这里呢?”<br>丹尼尔用他那“本来就是这样”的一贯口吻回答,“是为了太阳和月亮,是为了大家一起的快乐时光,是为了树荫、老人和小孩子,是为了秋天的色彩,是为了四季,这些还不够吗?”</p>
</blockquote>
<p>虽然是写给孩子的绘本,但却意外拥有不错的文学性,虽不如保尔的话那样激情澎湃,却恬淡中饱含睿智,读来直指人心,以至于让我玩味了许久。我当时并没有意识到这段话跟《红楼梦》的主旨也有几分贴合,只是觉得它美的像诗。</p>
<p>诗可以遣怀,却无法解忧,我不禁发出了一句龙场的追问:“圣人处此,更有何道?”</p>
<p>我这样讲并不是有意要引出王阳明,事实上我当时还没有意识到圣人是何物,也并未意识到圣人之言会是解决我烦恼的一把钥匙,虽然这个过程中确实有王阳明的影子,但我并不是突然就走进去的。我在冥思苦想无果后,走上了另一条路:<strong>了却遗憾!</strong>我选定的第一件,后来看也是唯一的一件事:读以前未读完的书!</p>
<p>当时读过的为数不多的书中,有很大一部分是半途而废的,有文学、通俗小说、历史读物等等。于是我暗自较劲,要将这些遗憾一扫而空,这些书单当中就有当年明月的《明朝那些事儿》。</p>
<p>《明朝》是我大学时读的书,但因当时只能借到其中一部分,读完之后也就作罢了。从这件事也看出了我当时得过且过、浑浑噩噩的心态,没有一点儿要找书读书的心思。不过书中写王守仁的部分给当时愚昧的我留下了相当深刻的印象,这自然要归功于当年明月不遗余力地大肆吹捧。我当时脑中的圣人除了孔孟,几乎想不到别人,对朱熹、二程的地位也没有丝毫认识。所以,当再次读到王守仁经天纬地、震古烁今的事迹之后,我终于按捺不住了:这样一号大人物,为什么我以前从没听过?</p>
<p>我清楚地记得,我当时没有去搜维基百科或者百度百科(后来看维基百科成为我快速扫盲的方式),而是购入了四部书——《传习录》、《传习录注疏》、《王文成公全集》、《阳明学述要》。然后郑重其事地作了一片名为《从此心向光明之学,做一个王学门人》的文章以彰显心志。不过很惭愧,这四部书我目前也只看完了一部半。钱穆先生的《阳明学述要》很薄,是优先读完的,简体译注版《传习录》却只读了一半。</p>
<p>读过一部分内容之后,我大体有两个感受:一是有相当多的内容读不太懂,二是感觉大部分都是修身养性的哲学。读不太懂是很正常的,毕竟我是个现代人,缺乏相关的背景知识,这无可厚非。后来辗转了解到,要想看懂书中的讨论和主张,必须清楚王阳明继承的什么,反对的是什么。王阳明的思想属于儒家阵营,反对同属于儒家阵营的朱熹。不难想见,“儒家”这个词在我脑海中一样是一片虚无。我本应像从前一样就此止步、放弃了事的,然而并没有,我在钱穆先生的书中读到了浓浓的人生哲学的味道,这与我思索已久的“人生的意义”有着很奇妙的呼应。于是,我马不停蹄地又看了几本钱穆先生的书,同时粗略读完了《四书》中的《大学》和《中庸》,又一鼓作气读完了冯友兰的《中国哲学简史》。</p>
<p>思想的源流一旦打开,就会变得周流无碍。那一刻,我意识到一件事:<strong>在思想一途,历史上的哲人已经将边界拓展到了足够广袤,只是我们生于其中而不自知。</strong>换句话说,你思考的任何人生问题,极大概率上已经有人思考过了,只等你耐心去发现。于是,我开始沿着兔子的细毛向上攀登了。</p>
<p>思想的接力就如同互联网上的爬虫一样求索无涯,远远没有停止。在此过程中,哲学、历史、社会学、经济学、文学等领域的书籍陆陆续续加入到我的阅读清单中,它们或在我的电子书中,或在我的书架上,林林总总共计四百余部。其中对我影响最大的是熊逸的书,他的书多论而少证,往往不避絮繁,带领你在思想的小径上左冲右突,时常就某一问题援引古今中外的不同观点,使其交火碰撞,或相融,或相斥,勾连万端;他的书没有学者的谆谆习气,不会正襟危坐地施加道德训诫,而是沿着逻辑链条理性地提出各种问题,并试图解答,而答案往往不止一种。可想而知,这正迎合了我彼时的阅读趣味,我正唯恐眼界不够高,阅读不够广,思想不够深,答案不够多呢。不出意外,我渐次读完了他绝大部分的著作,仍感到意犹未尽。在此期间,许多旧有的认知被颠覆,变得支离破碎;新的认知与观念不断形成,有时新认知又会在短时间内变成旧认知,被再次打破。</p>
<p>如果说我之前的探索开启了人生的思想启蒙运动的话,那么思想体系的建立就是在这破与立之间完成的。我从此知“世界何以为世界”,“历史何以为历史”,“我何以为我”,而王阳明似乎已经不再重要了,他已化作我思想坐标系中的一个点,再也不是之前悬浮的状态了。这场由灵魂追问引发的思想震颤铸就了我对世界的好奇心,让我极力地抓住兔子的细毛想要看清魔法师的脸,而震荡的余波到现在依然没有停歇,而且天高海阔,无远弗届,虽穷山距海,不能限也!</p>
<ol start="3">
<li></li>
</ol>
<p>我渐渐形成的好奇心不出意外地影响到了我从事的专业。</p>
<p>我是学计算机出身,但选择计算机专业的原因却很草率,仅仅是觉得电脑游戏好玩,也很自然地被现实打了脸。大学期间,我对计算机枯燥的课程完全提不起兴趣,仍旧徜徉于自己浮浅的精神世界里。</p>
<p>工作以后,我没有停止学习,但也仅仅是以实用为目的浅尝辄止,只为了解决工作中遇到的问题。几年后我迎来了思想蜕变的时刻,我的好奇心让我在计算机领域也产生了哲学三问,对于一种技术:“它是什么?它有着怎么样的历史?它未来会如何发展?”</p>
<p>思想驱动了行为,这个过程中我重温了大学里的计算机课程,同时也明白了大学何以要教这些枯燥的课程,最重要的是,我亲自为自己建立了计算机的世界观,亲手描摹出了计算机的坐标系。</p>
<p>我还有一个意外的感触:我越深入,就越能感受到计算机科学的魅力,我觉得它有时候美的像诗一样。这个世界诗与文章已经很美了,但有一些美需要你更加努力才能发现。这有些违反直觉,在我们刻板印象中计算机和数学一样,需要以强大的理性来对待,而艺术和一切美的东西都是感性的、疯狂的,甚至是幼稚的。但如果我们细心去了解一下的话,直觉往往都会被颠覆。</p>
<p>古希腊的伟大学者毕达哥拉斯不但是西方数学的始祖,同时也是西方音乐的始祖,他正是从竖琴的音色里得出了数学上一个又一个的创见;肖邦,这位以诗人气质著称于世的钢琴家,研究出了作曲的数学公式,他会根据公式而非感情来创作,创作出一串又一串使人浑然不觉生硬的数字,而那些数字就是引发灵魂震颤的乐章;巴赫,复调音乐的宗师巨匠,同样是在以精确的数学指导着精确的旋律对位。在古代西方世界里,艺术家们对数学的迷恋简直近乎迷信。画家也不例外,他们正是出于对几何学的深入研究才发明了独到的透视画法。</p>
<p>可见理性和美并不是完全对立的,计算机也可以产生艺术之美。但计算机的世界烟坡浩渺,横无际涯,是如此的汪洋恣肆,纷繁复杂。作为人类中的一个微不足道的个体,说它“累世不能通其学,当年不能究其礼”一点都不夸张。对于读书学习,苏轼曾经说过:“书富如入海,百货皆有,人之精力,不能兼收并取,但得其所欲求者尔。” 于是,走进计算机图书汪洋中的我,并没有去翻那些烫金的精装本,而是拿起了落满灰尘的线装书,因此我自称为“计算机故纸堆漫游者”,也仅仅是“但得其所欲求者尔”。</p>
<p>从离开校园到对计算机产生由衷的热爱,我花了 5 年的时间,此后又花了多年才让自己变成毕业时该有的样子。我曾经这样勉励自己:“<strong>我希望我35岁时能比肩25岁优秀的人,40岁时能比肩35岁优秀的人,45岁时能追上那些优秀的人。</strong>”我写这篇文章时已36岁,现在看来这样的期望还是过于乐观了,此处我要借用安德鲁·马维尔的诗句:“但我总是听到,背后隆隆逼近的时间的战车……”</p>
<p>无论如何,我对计算机产生了从单纯求知到由衷热爱的转变。当我还诧异于思想蜕变附带来的特殊人生体验时,我竟然从苏轼一篇名为《中庸论》的文章中读到了相似的感悟,只不过苏轼比我会总结,看问题比我更深刻,他在文中用“诚”和“明”两个概念阐述了求知和热爱的关系。这是我所说“你思考或者体验的任何问题,极大可能前人已经思考过了”的一个佐证。</p>
<blockquote>
<p>《记》曰:“自诚明谓之性,自明诚谓之教。 诚则明矣,明则诚矣。 ”夫诚者,何也?乐之之谓也。 乐之则自信,故曰诚。 夫明者,何也?知之之谓也。 知之则达,故曰明。 夫惟圣人,知之者未至,而乐之者先入,先入者为主,而待其余,则是乐之者为主也。 若夫贤人,乐之者未至,而知之者先入,先入者为主,而待其余,则是知之者为主也。 乐之者为主,是故有所不知,知之未尝不行。 知之者为主,是故虽无所不知,而有所不能行。 子曰:“知之者,不如好之者,好之者,不如乐之者。” 知之者与乐之者,是贤人、圣人之辨也。 好之者,是贤人之所由以求诚者也。</p>
</blockquote>
<p>苏轼先抛出了《中庸》的一段名言:“自诚明谓之性,自明诚谓之教。 诚则明矣,明则诚矣。”</p>
<p>这句话很玄妙,没有人能准确解释,即便古人也是连蒙带猜地去理解。大体上说,这句话在说“诚”和“明”的关系,先天禀赋和后天教养的关系。</p>
<p>但到底什么是“诚〞,什么又是“明”呢? 苏轼援引孔子的一句名言:“知之者不如好之者,好之者不如乐之者。“然后解释说 :“诚”就是“乐之”,明”就是“知之”。</p>
<p>简单讲,如果你对某事某物天生就有浓厚的兴趣,并乐此不疲,这就是“诚”。比如,你看到美女或者帅哥,自然就会产生怜爱之心,这不需要刻意学习,因为你天性如此,所以孔子会说“吾未见好德如好色者也”。</p>
<p>如果你在离开校园后,依然能够坚持学习各种专业知识,但究其原因,这份坚持很可能不是源自对知识的热爱,而仅仅是出于工作需要。你只是在求知的意义上获取到了知识,而不是天然热爱你的工作领域,这种情况就属于“明”。</p>
<p>“诚”是人与生俱来的特质,从“诚”的状态很容易到达“明”;而从“明”到“诚”却并不简单,你需要具备孜孜不倦的精神,持之以恒的努力,这就是“好之”,是到达“诚”的一种途径。苏轼用圣人和贤人在修养上的区别来阐发“诚”和“明”:圣人往往未知而先行,贤人能做到无所不知,但未必能行。简单说,圣人热爱,所以知之容易,贤人学而知之,但不一定能做到热爱,要抵达热爱的程度,还需要“好之”。</p>
<p>也就是说,如果你对某一领域有一份发自内心的热爱,那么很自然地,你会出于这份热爱,去认真学习相关的知识 ,从热爱到求知的这个过程,就是从“诚”到“明”的过程,也就是《中庸》所谓”自诚明”。这个过程是天性使然,所以”自诚明谓之性”。</p>
<p>相反,如果你并没有这份发自内心的热爱,只是在求知的意义上明白了知识是怎么一回事,那么你的认知就难免流于表面,失之肤浅。如果你并没有因此放弃,而是不断提升自己,不断加深理解的话,那么终有一天你学到的知识会内化成你的一部分,让你产生了发自内心的热爱。从求知到热爱的这个过程,就是从“明“到“诚”的过程,也就是《中庸》所谓“自明诚”。这个过程是教养使然,所以”自明诚谓之教”。</p>
<p>天性总是水到渠成,但教养在大多数情况下却是违逆人性的,然而真正的智慧都来自内心,所以“诚”很值得去追求。孔子的弟子一直跟随在孔子身边,很大程度上并不是因为还有未学到的儒家知识,而仅仅是想再多受一点孔子的熏陶,让所学的知识真正内化为自己的一部分。我之所以能“好之”以求“诚”,完全是思想蜕变作了第一推动力。</p>
<ol start="4">
<li></li>
</ol>
<p>熙宁二年,三十四岁的苏轼任满还朝,途径长安,寄住在好友石苍舒家中。石苍舒收藏字画的地方叫“醉墨堂”,他邀请苏轼为“醉墨堂”作诗,苏轼写了一首《石苍舒醉墨堂》,开头两句便是“人生识字忧患始,姓名粗记可以休”,这话听起来很没道理,为什么人生的忧患会从认字开始呢?</p>
<p>其实,苏轼的本意应该是规劝好友,不要为物所累,对字画倾注过多的热情,以免玩物丧志。这种思想在他后来写给驸马王诜的《宝绘堂记》中表达的更加清晰。不过我们也不妨跟随字面的意思来理解,当一个人识文断字之后,对周围的事物产生了深刻的认识和理解,这份认知的增进往往伴随着无尽的焦虑与忧患。因此,思想变化所带来的影响并不都是积极的。</p>
<p>二十八岁以前的我,可以说是快乐的,无甚忧虑的,我像一只蚕蛹,本应在生活的茧房里优游卒岁,不料“心事浩渺连广宇,于无声处听惊雷”,这只蚕蛹偶然间破茧而出,竟有了探索世界的能力,在游历过一番之后,才蓦然发现自己在食物链中的位置,原本多姿多彩的世界一时间蒙上了灰色的影子。</p>
<p>但这并不是最令人沮丧的,思想重塑带来了“今是而昨非”的觉悟,大有“朝闻道,夕死可以”的气势。但转念之间,又不禁生出了“后之视今亦由今之视昔”的担忧。试想,未来的我很可能会视今日之坚持为谬误,我此刻的努力,是否还能保持其意义与价值?</p>
<p>这是一个很严峻的问题!</p>
<p>庄子的《逍遥游》似乎可以给出一种态度,《逍遥游》讲了一个“人如何才能逍遥”的道理,文章用很大的篇幅对比大鹏和两只小鸟,来说明“小知不及大知”,而且写的奇伟瑰丽,文学性很强,让人以为庄子是在为大鹏摇旗呐喊,同时贬低小鸟。那么,在这个故事中,庄子除了做事实陈述之外,有没有下价值判断呢?也就是说,除了小鸟和大鹏的客观不同之外,庄子有没有认为小鸟不如大鹏呢?</p>
<blockquote>
<p>故夫知效一官,行比一乡,德合一君,而征一国者,其自视也,亦若此矣。而宋荣子犹然笑之。且举世誉之而不加劝,举世非之而不加沮,定乎内外之分,辩乎荣辱之境,斯已矣。彼其于世,未数数然也。虽然,犹有未树也。夫列子御风而行,泠然善也,旬有五日而后反。彼于致福者,未数数然也。此虽免乎行,犹有所待者也。若夫乘天地之正,而御六气之辩,以游无穷者,彼且恶乎待哉?故曰:至人无己,神人无功,圣人无名。</p>
</blockquote>
<p>庄子说,有些人论才智可以做官,论行为符合一乡的道德标准,论德行能投合一个君王的心意的,论能力能够取得全国信任,但这些人跟小鸟是一个档次的,所以宋荣子才会嗤笑他们。显然宋荣子的境界更高,但列子比宋荣子还要高,列子出行需要靠风,所以还有所依恃,这叫“有所待”。有所待就不够逍遥,真正的逍遥是“无所待”,是要“乘天地之正,而御六气之辩,以游无穷”。</p>
<p>可见,庄子的意思很明显:小不如大,有待不如无待。所以仅就“逍遥游”的精神追求而言,在大鹏与小鸟之间,你应该努力学大鹏,在“知效一官”者流与宋荣子之间,你应该努力学宋荣子。总之,即便你穷尽一生都无法达到最高境界,你也应该努力靠近它。</p>
<p>有人曾对人类的科学抱有十分悲观的论调,说人类的科技进步在上帝看来,只不过是地球上两个相差不过一寸的小人,高一寸还是矮一寸,根本毫无区别。胡适反驳这种观点,说人类文明的进步所争的就是这一寸的长短,进一寸自然有这一寸的欢愉。胡适是一个极度乐观主义者,字里行间常常洋溢着乐观和自信,他在《科学的人生观》中慷慨激昂地说道:“朝夕地去求真理,不一定要成功,因为真理无穷,宇宙无穷:我们去寻求,是尽一点责任,希望在总分上,加上万万分之一。胜固是可喜,败也不足忧 …… 庄子虽有 ‘吾生也有涯,而知也无涯,以有涯逐无涯,殆已’的话头,但是我们还要向上做去,得一分就是一分、一寸就是一寸 …… 因为真理无穷,趣味无穷,进步快活也无穷尽。”</p>
<p>没错,胜固然欣,败亦可喜,怕什么真理无穷,进一寸有一寸的欢喜。</p>
<ol start="5">
<li></li>
</ol>
<p>回到最初的问题,人生的意义是什么?</p>
<p>其实,这个问题并没有标准答案,是一个应然问题,是价值判断,是高度主观的,任何答案都对,任何答案都不对。</p>
<p>我现阶段的认知是:人生根本没有任何意义,人生只不过是一场生物学的事实罢了!</p>
<p>如果你退而求其次,问我要做一个怎样的人?我已列举了几种人生态度,我当然推荐保尔柯察金的方案,但是,如果你行有余力,我的建议是:你不必成为其中某一种人,你可以成为他们所有人,择其善者而从之。毕竟,如果每个问题都有定论,那世界将是多么无趣啊!</p>
<p>最后,让我用《列子·杨朱》中的一个故事结束整篇文章:</p>
<blockquote>
<p>昔者宋国有田夫,常衣缊黂(乱麻为絮的衣服),仅以过冬。暨春冬作,自曝于日,不知天下之有广厦隩室,绵纩(新丝棉)狐貉。顾谓其妻曰:‘负日之暄,人莫知者,以献吾君,将有重赏。</p>
</blockquote>
<p>这个故事里,有一个宋国农夫,常常披着破麻絮布勉强熬过冬天。等到了春天,在田里干活,他独自晒太阳,觉得很暖和。他压根儿不知道世上还有高楼大厦,深宅大院,丝绵衣服狐皮袍子。他回家对妻子说:“人们还不知道背晒太阳就会暖和。把用太阳取暖的方法献给国君,一定会得重赏。”</p>
<p>这则故事为我们留下了“负暄献御”这个并不常用的成语。</p>
<p>而我,就是那个献御的宋国农夫。</p>
</div>
<footer class="post-footer">
<div class="post-eof"></div>
</footer>
</article>
</div>
<div class="post-block">
<article itemscope itemtype="http://schema.org/Article" class="post-content" lang="">
<link itemprop="mainEntityOfPage" href="http://liupzmin.com/2024/03/21/golang/for-range-copy/">
<span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
<meta itemprop="image" content="/images/gzh.jpg">
<meta itemprop="name" content="巴流">
</span>
<span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
<meta itemprop="name" content="兔子先生">
<meta itemprop="description" content="左手人文 | 右手科技">
</span>
<span hidden itemprop="post" itemscope itemtype="http://schema.org/CreativeWork">
<meta itemprop="name" content="undefined | 兔子先生">
<meta itemprop="description" content="">
</span>
<header class="post-header">
<h2 class="post-title" itemprop="name headline">
<a href="/2024/03/21/golang/for-range-copy/" class="post-title-link" itemprop="url">Go 语法糖 for range 中的 copy 问题</a>
</h2>
<div class="post-meta-container">
<div class="post-meta">
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="far fa-calendar"></i>
</span>
<span class="post-meta-item-text">发表于</span>
<time title="创建时间:2024-03-21 22:17:31" itemprop="dateCreated datePublished" datetime="2024-03-21T22:17:31+08:00">2024-03-21</time>
</span>
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="far fa-calendar-check"></i>
</span>
<span class="post-meta-item-text">更新于</span>
<time title="修改时间:2024-08-02 12:16:16" itemprop="dateModified" datetime="2024-08-02T12:16:16+08:00">2024-08-02</time>
</span>
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="far fa-folder"></i>
</span>
<span class="post-meta-item-text">分类于</span>
<span itemprop="about" itemscope itemtype="http://schema.org/Thing">
<a href="/categories/golang/" itemprop="url" rel="index"><span itemprop="name">golang</span></a>
</span>
</span>
</div>
</div>
</header>
<div class="post-body" itemprop="articleBody">
<p>Go 的赋值、参数传递都是值传递,也就是说你得到的是一份 copy,对于如下的 for range 循环:</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> someslice = []<span class="type">int</span>{<span class="number">0</span>,<span class="number">1</span>}</span><br><span class="line"><span class="keyword">for</span> i, v := <span class="keyword">range</span> someslice {</span><br><span class="line"> <span class="comment">// do something</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>短变量<code>v</code>是<code>someslice</code>中的元素的 copy,在 Go 1.22 以前<code>v</code>只会创建一次,每次循环会复用这一变量,从 1.22 起每次循环会创建新的变量。</p>
<p>现在的问题是: range 后的<code>someslice</code>还是原来的<code>someslice</code>吗?会不会也是一个 copy 呢?</p>
<p>你能猜出下面一段代码的打印结果吗?</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">fib := []<span class="type">int</span>{<span class="number">0</span>, <span class="number">1</span>}</span><br><span class="line"><span class="keyword">for</span> i, f1 := <span class="keyword">range</span> fib {</span><br><span class="line"> f2 := fib[i+<span class="number">1</span>]</span><br><span class="line"> fib = <span class="built_in">append</span>(fib, f1+f2)</span><br><span class="line"> <span class="keyword">if</span> f1+f2 > <span class="number">100</span> {</span><br><span class="line"> <span class="keyword">break</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line">fmt.Println(fib)</span><br></pre></td></tr></table></figure>
<p>结果是:<code>[0 1 1 2]</code>,是不是不像推想的那样<code>fib</code>中的元素会超过 100?但如果分别在 for 循环的前、中、后打印一下<code>fib</code>的地址,你会发现地址没有变(在这个例子中底层数组是会变得,你可以使用<code>fmt.Printf("fib1:%p\n", fib)</code>来验证,对切片使用<code>%p</code>打印会打印第0个元素的地址)。</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">fib := []<span class="type">int</span>{<span class="number">0</span>, <span class="number">1</span>}</span><br><span class="line">fmt.Printf(<span class="string">"fib outer:%p\n"</span>, &fib)</span><br><span class="line"><span class="keyword">for</span> i, f1 := <span class="keyword">range</span> fib {</span><br><span class="line"> fmt.Printf(<span class="string">"fib inner:%p, f1:%p\n"</span>, &fib, &f1)</span><br><span class="line"> f1, f2 := fib[i], fib[i+<span class="number">1</span>]</span><br><span class="line"> fib = <span class="built_in">append</span>(fib, f1+f2)</span><br><span class="line"> <span class="keyword">if</span> f1+f2 > <span class="number">100</span> {</span><br><span class="line"> <span class="keyword">break</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line">fmt.Printf(<span class="string">"fib outer:%p\n"</span>, &fib)</span><br><span class="line">fmt.Println(fib)</span><br><span class="line"></span><br><span class="line"><span class="comment">//打印结果</span></span><br><span class="line">fib outer:<span class="number">0xc00011c000</span></span><br><span class="line">fib inner:<span class="number">0xc00011c000</span>, f1:<span class="number">0xc000110030</span></span><br><span class="line">fib inner:<span class="number">0xc00011c000</span>, f1:<span class="number">0xc000110038</span></span><br><span class="line">fib outer:<span class="number">0xc00011c000</span></span><br><span class="line">[<span class="number">0</span> <span class="number">1</span> <span class="number">1</span> <span class="number">2</span>]</span><br></pre></td></tr></table></figure>
<p>这是不是说明 range 就是在遍历原<code>fib</code>本身呢?如果是遍历原<code>fib</code>,又为什么这里只循环了 2 次呢?</p>
<p>让我们看看<span class="exturl" data-url="aHR0cHM6Ly9nby5kZXYvcmVmL3NwZWMjRm9yX3N0YXRlbWVudHM=">spec<i class="fa fa-external-link-alt"></i></span>中的说明:</p>
<blockquote>
<p>The range expression x is evaluated once before beginning the loop, with one exception: if at most one iteration variable is present and len(x) is constant, the range expression is not evaluated.</p>
</blockquote>
<p>这里的意思是:<strong>在进入循环之前,range 表达式只会计算一次!</strong>但这个<code>evaluate</code>具体指何意,spec 没有解答,看来只能在编译器源码中寻找答案了,我是没有大海捞针的精力了,不过已经有人替我们做了,美中不足的是参考的 gcc 的代码,不过想来都遵循语言规约的话,<span class="exturl" data-url="aHR0cHM6Ly9naXRodWIuY29tL2dvbGFuZy9nby9ibG9iL2VhMDIwZmYzZGU5NDgyNzI2Y2U3MDE5YWM0M2MxZDMwMWNlNWUzZGUvc3JjL2NtZC9jb21waWxlL2ludGVybmFsL2djL3JhbmdlLmdvI0wxNjk=">行为方式总是大差不差的<i class="fa fa-external-link-alt"></i></span>,来看看 gcc 的 Go 编译器源码中 range 子句的注释:</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">// Arrange to do a loop appropriate for the type. We will produce</span><br><span class="line">// for INIT ; COND ; POST {</span><br><span class="line">// ITER_INIT</span><br><span class="line">// INDEX = INDEX_TEMP</span><br><span class="line">// VALUE = VALUE_TEMP // If there is a value</span><br><span class="line">// original statements</span><br><span class="line">// }</span><br></pre></td></tr></table></figure>
<p>可见 range 循环仅仅是 C-style 循环的语法糖,所以当你 range 一个 array 时:</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">// The loop we generate:</span><br><span class="line">// len_temp := len(range)</span><br><span class="line">// range_temp := range</span><br><span class="line">// for index_temp = 0; index_temp < len_temp; index_temp++ {</span><br><span class="line">// value_temp = range_temp[index_temp]</span><br><span class="line">// index = index_temp</span><br><span class="line">// value = value_temp</span><br><span class="line">// original body</span><br><span class="line">// }</span><br></pre></td></tr></table></figure>
<p>range slice 时:</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">// for_temp := range</span><br><span class="line">// len_temp := len(for_temp)</span><br><span class="line">// for index_temp = 0; index_temp < len_temp; index_temp++ {</span><br><span class="line">// value_temp = for_temp[index_temp]</span><br><span class="line">// index = index_temp</span><br><span class="line">// value = value_temp</span><br><span class="line">// original body</span><br><span class="line">// }</span><br></pre></td></tr></table></figure>
<p>我们可以从中得到至少4点启示:</p>
<ol>
<li>循环最终都是 C-style 的。</li>
<li>循环遍历的对象都会被赋值给一个临时变量。</li>
<li>由第 2 点可知,range 一个数组的成本要大于 range 切片。</li>
<li>for range 居然涉及到 2 次 copy,一次是 copy 迭代的对象,一次是集合中的元素 copy 到临时变量。</li>
</ol>
<p>我们还原一下开篇提到的代码,大致是如下的样子:</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">fib := []<span class="type">int</span>{<span class="number">0</span>, <span class="number">1</span>}</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> f1 <span class="type">int</span></span><br><span class="line"><span class="comment">// copy 迭代对象</span></span><br><span class="line">temp := fib</span><br><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i < <span class="built_in">len</span>(temp); i++ {</span><br><span class="line"> <span class="comment">// copy 元素</span></span><br><span class="line"> f1 = temp[i]</span><br><span class="line"> f2 := fib[i+<span class="number">1</span>]</span><br><span class="line"> fib = <span class="built_in">append</span>(fib, f1+f2)</span><br><span class="line"> <span class="keyword">if</span> f1+f2 > <span class="number">100</span> {</span><br><span class="line"> <span class="keyword">break</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line">fmt.Println(fib)</span><br></pre></td></tr></table></figure>
<p>这就解释了代码为何只迭代两次,<code>fib</code>在循环开始前被复制,循环次数就被固定为 2 了。</p>
<p>因此,如果我们迭代一个切片,并且想修改里面的东西,除了在切片中存储指针以外,使用传统的 C-style 风格的循环也是个不错的选择。</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> i := <span class="number">0</span>; i < <span class="built_in">len</span>(slice); i++ { ... }</span><br></pre></td></tr></table></figure>
<p>如果我遍历的是个 map 呢?众所周知,map 是一个指针,range 计算就算 copy 也是 copy 得指针,遍历的时候仍然是同一个 map,事实上 spec 上也说明了这种情况:</p>
<blockquote>
<p>If a map entry that has not yet been reached is removed during iteration, the corresponding iteration value will not be produced. If a map entry is created during iteration, that entry may be produced during the iteration or may be skipped. </p>
</blockquote>
<p>概括来说,你可以在 range 循环中对 map 进行增删,删掉的元素不会在接下来被遍历到,增加的元素则不一定,也许会被遍历,也许不会,这是由 map 底层使用哈希表实现以及随机遍历机制决定的。</p>
<p><strong>参考文献:</strong></p>
<ol>
<li><span class="exturl" data-url="aHR0cHM6Ly9nby5kZXYvcmVmL3NwZWMjRm9yX3N0YXRlbWVudHM=">The Go Programming Language Specification<i class="fa fa-external-link-alt"></i></span></li>
<li><span class="exturl" data-url="aHR0cHM6Ly9nYXJiYWdlY29sbGVjdGVkLm9yZy8yMDE3LzAyLzIyL2dvLXJhbmdlLWxvb3AtaW50ZXJuYWxzLw==">Go Range Loop Internals<i class="fa fa-external-link-alt"></i></span></li>
<li><span class="exturl" data-url="aHR0cHM6Ly93d3cuY2FsaG91bi5pby9kb2VzLXJhbmdlLWNvcHktdGhlLXNsaWNlLWluLWdvLw==">Does Go's range Copy a Slice Before Iterating Over It?<i class="fa fa-external-link-alt"></i></span></li>
</ol>
</div>
<footer class="post-footer">
<div class="post-eof"></div>
</footer>
</article>
</div>
<div class="post-block">
<article itemscope itemtype="http://schema.org/Article" class="post-content" lang="">
<link itemprop="mainEntityOfPage" href="http://liupzmin.com/2024/01/03/golang/impression-of-go/">
<span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
<meta itemprop="image" content="/images/gzh.jpg">
<meta itemprop="name" content="巴流">
</span>
<span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
<meta itemprop="name" content="兔子先生">
<meta itemprop="description" content="左手人文 | 右手科技">
</span>
<span hidden itemprop="post" itemscope itemtype="http://schema.org/CreativeWork">
<meta itemprop="name" content="undefined | 兔子先生">
<meta itemprop="description" content="">
</span>
<header class="post-header">
<h2 class="post-title" itemprop="name headline">
<a href="/2024/01/03/golang/impression-of-go/" class="post-title-link" itemprop="url">谈 Go 的使用感受</a>
</h2>
<div class="post-meta-container">
<div class="post-meta">
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="far fa-calendar"></i>
</span>
<span class="post-meta-item-text">发表于</span>
<time title="创建时间:2024-01-03 20:17:31" itemprop="dateCreated datePublished" datetime="2024-01-03T20:17:31+08:00">2024-01-03</time>
</span>
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="far fa-calendar-check"></i>
</span>
<span class="post-meta-item-text">更新于</span>
<time title="修改时间:2024-08-02 12:16:16" itemprop="dateModified" datetime="2024-08-02T12:16:16+08:00">2024-08-02</time>
</span>
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="far fa-folder"></i>
</span>
<span class="post-meta-item-text">分类于</span>
<span itemprop="about" itemscope itemtype="http://schema.org/Thing">
<a href="/categories/golang/" itemprop="url" rel="index"><span itemprop="name">golang</span></a>
</span>
</span>
</div>
</div>
</header>
<div class="post-body" itemprop="articleBody">
<p>假设让你在三种不同的条件下完成一副画,要求笔尖接触纸面到离开纸面即算一笔,三种条件分别是:只能落笔 1、100、10000 次。你觉得对于画家来讲,哪一种方式更困难?</p>
<p>显然,在只能落笔一次的情况下你需要高超的技巧才能一笔画完,而多次落笔会让你驾驭地轻松一点,更多的落笔机会就允许你从容地完成画作。</p>
<p>画是一种空间和色彩交织的复杂整体,其创作过程就是表现这个复杂整体的过程,当你用线性的笔法来创作这个浑然的整体时,难度就凸显出来了,即便天才画家也未必能玩转这种技巧,但当你手中可用的线条足够多时,你就可以轻易地组合成复杂的整体了,不单单是画的创作,在文章以及小说的创作上也类似。</p>
<p>叶圣陶先生在他的文章创作文集中对写作有过如下一段高屋建瓴的论述:<strong>“我们还期望能够组成调顺的‘语句’,调顺的‘篇章’。怎样叫作调顺呢?内面的意思情感是浑凝的,有如球,在同一瞬间可以感知整个的含蕴;而语言文字是连续的,有如线,须一贯而下,方能表达全体的内容。作文同说话一样,是将线表球的工夫,能够经营到通体妥帖,让别人看了便感知我们内面的意思情感,这就叫作调顺。”</strong></p>
<p>因此,才有人说:“<strong>写作,是一场孤独的旅程。</strong>”</p>
<p>叶圣陶先生将写作喻为<strong>“线表球”</strong>的功夫,这是很高明的见识,第一流的见解。我们再来看编程本身,就是将你一团浑凝的、整体的思绪,用指令的方式一丝一缕叙述出来,你要小心地安排事件的进展,巧妙地处理那些同时发生的事情,这跟写作的思路是暗合的,所以我常说:“每个人都能识文断字,但文学家总是凤毛麟角,编程就像作文,计算机语言的语法不难掌握,但天才的程序员却不可多得。” 道理其实一样,难点在于如何用有限的线条去勾勒复杂的整体。</p>
<p>我们这里不谈艺术不谈性能,仅仅从创作者与写作者的心智负担轻重角度讨论。对于写作,你当然无法同时去写几件事情,我在读金庸先生的作品时曾留意过他的叙事手法:<strong>剧情先是沿着主线流淌,因为某些事件的发生,几个人物分离,主线会择其中一人继续流淌,在未来的某个时间点会再次汇集,此时金庸先生会采用中断的方式,倒回去叙述另一人物的剧情,一直到交叉点为止。</strong>当然有些作者会采用多线叙事法,几处剧情同时进行,读者要在这几处剧情中几进几出,最后在某处汇集,但在作者的角度就是类似于一个单核 CPU 进行并发。</p>
<p>显然金庸老爷子的写作方式难度更高,更考验情节的安排与事件的把握,换句话说,小说是一个浑圆的整体,金庸先生将其用一条行进的流来表现复杂的整体;而多线叙事法则相对简单些,只要分开叙事,在关键点集中叙事即可。所以我们不难推想一下,把这种叙事推向极端:支线之间没有重合!作者只要分别写几则叙事短篇即告完成,当然这种书是没有意义的,我们只是来说明这种方式是用多个行进的流来表现复杂的整体,谁都不会否认一笔写完一个字和一镜到底的艺术难度!不过如果人的大脑可以并行工作,我想肯定多线叙事的小说会更受欢迎,只要作者调整一下文字结构,使这些线只在剧情汇集点交叉,不要为了照顾串行的大脑而做形式上的交叉。</p>
<p>现实世界是时间和空间的复杂结合,而最初的计算机程序是单进程,只能线性地表达复杂事物,也就是你要在单个进程流里做所有的事,这种一镜到底的功夫是需要一些艺术规划的;而多进程和多线程让这种表达轻松了许多,你可以几十镜、几百镜一起运用,通过剪辑来表现那个浑凝的整体,但是对于普通人来讲依然困难,因为管理进线程有一套复杂的接口,关键是你要有所节制,要小心地控制进线程的数量,不能屁大点事都要弄个线程去做,你仍然需要在几条或者几十条,几百条线程流中为它找一个位置,因为房子不够多,该挤的还是要挤挤。</p>
<p>Go 语言的协程极大地拓展了这个上限,使得表达复杂事物变得简单了。大部分的场景你都可以给每一个小事安排一个房子,你不用再为了给它寻找位置而煞费苦心,你可以用大量的线条去勾勒一个复杂的整体,该添一笔的地方千万不要吝啬。<strong>毫无疑问,这降低了你的创作难度,你可以肆无忌惮地去表达你心中那个浑凝的整体,用一种近似浑凝的方式!</strong></p>
</div>
<footer class="post-footer">
<div class="post-eof"></div>
</footer>
</article>
</div>
<div class="post-block">
<article itemscope itemtype="http://schema.org/Article" class="post-content" lang="">
<link itemprop="mainEntityOfPage" href="http://liupzmin.com/2023/12/17/theory/terminal-buffer-io/">
<span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
<meta itemprop="image" content="/images/gzh.jpg">
<meta itemprop="name" content="巴流">
</span>
<span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
<meta itemprop="name" content="兔子先生">
<meta itemprop="description" content="左手人文 | 右手科技">
</span>
<span hidden itemprop="post" itemscope itemtype="http://schema.org/CreativeWork">
<meta itemprop="name" content="undefined | 兔子先生">
<meta itemprop="description" content="">
</span>
<header class="post-header">
<h2 class="post-title" itemprop="name headline">
<a href="/2023/12/17/theory/terminal-buffer-io/" class="post-title-link" itemprop="url">终端闲思录(2)- 终端与缓冲的关系</a>
</h2>
<div class="post-meta-container">
<div class="post-meta">
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="far fa-calendar"></i>
</span>
<span class="post-meta-item-text">发表于</span>
<time title="创建时间:2023-12-17 10:34:44" itemprop="dateCreated datePublished" datetime="2023-12-17T10:34:44+08:00">2023-12-17</time>
</span>
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="far fa-calendar-check"></i>
</span>
<span class="post-meta-item-text">更新于</span>
<time title="修改时间:2024-08-02 12:16:16" itemprop="dateModified" datetime="2024-08-02T12:16:16+08:00">2024-08-02</time>
</span>
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="far fa-folder"></i>
</span>
<span class="post-meta-item-text">分类于</span>
<span itemprop="about" itemscope itemtype="http://schema.org/Thing">
<a href="/categories/terminal/" itemprop="url" rel="index"><span itemprop="name">terminal</span></a>
</span>
,
<span itemprop="about" itemscope itemtype="http://schema.org/Thing">
<a href="/categories/terminal/computer-theory/" itemprop="url" rel="index"><span itemprop="name">computer theory</span></a>
</span>
,
<span itemprop="about" itemscope itemtype="http://schema.org/Thing">
<a href="/categories/terminal/computer-theory/buffer-io/" itemprop="url" rel="index"><span itemprop="name">buffer io</span></a>
</span>
</span>
</div>
</div>
</header>
<div class="post-body" itemprop="articleBody">
<p>我们已经知道标准三剑客(标准输入、标准输出、标准错误)的本质是文件描述符,其连接的目的地可以是任意类型的文件,终端只是常用的目的地之一。那么,当目的地类型不同,IO 的行为是否也有所不同呢?那位架构师所说<strong>“控制台是同步的”</strong>是否过于危言耸听了呢?</p>
<p>让我们看回作为“Linux 一切皆文件”的通用 I/O 接口:</p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><unistd.h></span></span></span><br><span class="line"><span class="type">ssize_t</span> <span class="title function_">read</span><span class="params">(<span class="type">int</span> fd, <span class="type">void</span> *buffer, <span class="type">size_t</span> count)</span>;</span><br><span class="line"> Returns number of bytes read, <span class="number">0</span> on EOF, or –<span class="number">1</span> on error</span><br><span class="line"> </span><br><span class="line"><span class="type">ssize_t</span> <span class="title function_">write</span><span class="params">(<span class="type">int</span> fd, <span class="type">void</span> *buffer, <span class="type">size_t</span> count)</span>;</span><br><span class="line"> Returns number of bytes written, or –<span class="number">1</span> on error</span><br></pre></td></tr></table></figure>
<p>这是 glibc 对系统调用<code>read</code>、<code>write</code>的封装,大部分应用的 IO 都是对这两个封装函数的调用,即便是不使用 C 库的语言,其标准库也提供对系统调用<code>read</code>、<code>write</code>的封装,比如 Go 语言,其标准库底层直接对接的系统调用,与 glibc 处于同一层级。</p>
<p>从接口定义可知,读与写都需要传入一个存储读写内容的缓冲区指针<strong>buffer</strong>以及存储内容大小的<strong>count</strong>,显而易见的是:在所需传输内容大小一定的情况下,<strong>buffer</strong>越小,对<code>read</code>、<code>write</code>的调用次数越多,反之则调用次数越少,我们来看一个不同 buffer 大小对传输时间影响的例子:</p>
<p><img data-src="https://qiniu.liupzmin.com/copy-100m.png" alt="图 1-1 复制 100MB 大小的文件所需时间"></p>
<p>读和写大体涉及三块时间:<strong>系统调用的时间</strong>、<strong>内核与用户空间数据复制的时间</strong>、<strong>内核和磁盘交互的时间</strong>。可见系统调用的成本还是比较可观的,当 <code>BUF_SIZE</code> 增长到 4096 的时候,总耗时趋于稳定,<code>BUF_SIZE</code> 的增加对性能的提升不再显著,这是因为与其它两个时间耗时相比,系统调用的时间成本已经微不足道了。</p>
<p>上面的例子混合了读与写,初次操作不可不免的需要从磁盘传输数据到页高速缓存中,让我们再看一个只有写的例子:</p>
<p><img data-src="https://qiniu.liupzmin.com/write-100m.png" alt="图 1-2 写一个 100MB 大小的文件所需的时间"></p>
<p>在 Linux 中,写是异步的,内容会先进入页高速缓存,之后<code>write</code>系统调用返回,真正落盘的操作由内核异步完成,因此只要内存充足,<code>write</code>的性能总是可以得到保证的。</p>
<blockquote>
<p>注:图中两例引用自<span class="exturl" data-url="aHR0cHM6Ly9ib29rLmRvdWJhbi5jb20vc3ViamVjdC8yNTgwOTMzMC8=">Linux/UNIX系统编程手册<i class="fa fa-external-link-alt"></i></span>,用于说明系统调用的昂贵,我偷懒没有自己写例子运行。</p>
</blockquote>
<p>总之,如果有大量内容需要使用 I/O 接口传输,或者需要长时间不定时调用 I/O 接口,通过采用合理的大块空间来缓冲数据,以减少系统调用的次数,可以极大地提高 I/O 的性能,此 glibc 中 stdio 之所为也!</p>
<p>C 库(Linux 中为 glibc)将文件读写抽象成了一个名为<code>FILE*</code>的流(stream,标准三剑客会被抽象为 stdin、stdout、stderr),其中就包含我们上面提到的缓冲处理,这避免了程序员自行处理数据缓冲。C 库有三种缓冲类型:</p>
<ul>
<li><strong>_IOFBF(全缓冲)</strong>。单次读写数据的大小与缓冲区大小相同,指代磁盘文件的流默认采用此模式。</li>
<li><strong>_IOLBF(行缓冲)</strong>。对于写入,在遇到换行符时才执行(除非缓冲区已填满);对于读取,每次读取一行数据。当连接终端时默认采取行缓冲。</li>
<li><strong>_IONBF(无缓冲)</strong>。不对 I/O 进行缓冲,每个 stdio 库函数将立即调用<code>read</code>、<code>write</code>,连接到终端的标准错误即是这种类型。</li>
</ul>
<p>用一个简单的程序来测试终端对 C 库缓冲的影响:</p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><unistd.h></span></span></span><br><span class="line"><span class="type">void</span> _isatty();</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">()</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> (;;) {</span><br><span class="line"> _isatty();</span><br><span class="line"> sleep(<span class="number">1</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> _isatty(){</span><br><span class="line"> <span class="keyword">if</span> (isatty(fileno(<span class="built_in">stdout</span>))) {</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"stdout is connected to a terminal (line buffered)\n"</span>);</span><br><span class="line"> <span class="built_in">fprintf</span>(<span class="built_in">stderr</span>,<span class="string">"an error painted to stderr\n"</span>);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"stdout is not connected to a terminal (fully buffered)\n"</span>);</span><br><span class="line"> <span class="built_in">fprintf</span>(<span class="built_in">stderr</span>,<span class="string">"an error painted to stderr, but redirected\n"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>请分别<strong>在终端中直接运行</strong>(./isatty)和<strong>重定向到文件运行</strong>(./isatty > isatty.log 2>&1 &),观察终端和 isatty.log 文件的输出,会发现如下现象:</p>
<ol>
<li>重定向(不再指向终端)会让标准输出变为全缓冲。</li>
<li>重定向对 stderr 无影响,默认情况下依然是无缓冲。</li>
</ol>
<p>由此可知,<strong>当向标准输出写日志时,其缓冲行为与是否连接到终端有关,当作为后台服务运行时,即便将日志打到标准输出,写日志的行为也是有缓冲的。至于标准错误,如果对错误信息没有即时要求,也是可以调整其缓冲模式的,不必依赖默认设置。</strong></p>
<p>当然,这是 C 库的做法,换一种不依赖 C 库的语言,行为可能就会不同,我们来看一下 Go 语言的表现(我总是举 Go 语言的例子,熟悉是一方面,更重要的是,Go 与 C 库脱离的很彻底):</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line"> <span class="string">"bufio"</span></span><br><span class="line"> <span class="string">"fmt"</span></span><br><span class="line"> <span class="string">"log/slog"</span></span><br><span class="line"> <span class="string">"os"</span></span><br><span class="line"> <span class="string">"time"</span></span><br><span class="line"></span><br><span class="line"> <span class="string">"golang.org/x/term"</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">main</span><span class="params">()</span></span> {</span><br><span class="line"> w := bufio.NewWriter(os.Stdout)</span><br><span class="line"> h := slog.NewJSONHandler(w, <span class="literal">nil</span>)</span><br><span class="line"></span><br><span class="line"> logger := slog.New(h)</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 检查标准输出是否连接到终端</span></span><br><span class="line"> a := <span class="function"><span class="keyword">func</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">if</span> term.IsTerminal(<span class="type">int</span>(os.Stdout.Fd())) {</span><br><span class="line"> fmt.Printf(<span class="string">"标准输出连接到终端--from fmt\n"</span>)</span><br><span class="line"> fmt.Fprintf(os.Stderr, <span class="string">"标准错误连接到终端--from fmt\n"</span>)</span><br><span class="line"> slog.Info(<span class="string">"标准输出连接到终端--from slog"</span>)</span><br><span class="line"> logger.Info(<span class="string">"标准输出连接到终端--from slog json logger"</span>)</span><br><span class="line"></span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> fmt.Printf(<span class="string">"标准输出未连接到终端--from fmt\n"</span>)</span><br><span class="line"> fmt.Fprintf(os.Stderr, <span class="string">"标准错误未连接到终端--from fmt\n"</span>)</span><br><span class="line"> slog.Info(<span class="string">"标准输出未连接到终端--from slog"</span>)</span><br><span class="line"> logger.Info(<span class="string">"标准输出未连接到终端--from slog json logger"</span>)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> {</span><br><span class="line"> a()</span><br><span class="line"> time.Sleep(<span class="number">1</span> * time.Second)</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure>
<p>请再次分别<strong>在终端中直接运行</strong>(./isatty)和<strong>重定向到文件运行</strong>(./isatty > isatty.log 2>&1 &),观察终端和 isatty.log 文件的输出,会发现如下现象:</p>
<ol>
<li><p>无论是否连接到终端,对标准输出和标准错误的输出行为不会发生任何变化</p>
</li>
<li><p>想要异步写入,需要自行构建带缓冲的 logger,如此例中的:</p>
<figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">w := bufio.NewWriter(os.Stdout)</span><br><span class="line">h := slog.NewJSONHandler(w, <span class="literal">nil</span>)</span><br><span class="line"></span><br><span class="line">logger := slog.New(h)</span><br></pre></td></tr></table></figure>
<p>而 slog 默认的 logger 是写入 stderr 的,不信你可以运行<code>./isatty > isatty.log &</code>仅仅重定向标准输出试试。</p>
</li>
</ol>
<p>产生这种现象的原因是 Go 标准库并没有像 C 库那样对标准库的 I/O 大包大揽,文件流<code>*os.File</code>是无缓冲的,Go 标准库函数的 I/O 基本都是基于接口,且提供有实现了缓冲 I/O 以及 I/O 接口的<code>bufio</code>包。</p>
<p>上面例子中的 json logger 就是使用<code>bufio</code>基于标准输出创建了一个带缓冲的<code>writer</code>,而 slog 包中创建 Handler 仅需传入实现<code>writer</code>接口的对象即可,因此我们得到一个带有缓冲的 logger。</p>
<p><img data-src="https://qiniu.liupzmin.com/c-io-summary.png" alt="图 1-3 I/O 缓冲"></p>
<p>不论标准库使用如何方式提供缓冲,其目的始终是减少系统调用,图 1-3 以 C 库为例展示了这种 I/O 模型,<strong>使用标准库函数将日志写入标准输出,是可以设置合理的缓冲区的,并不存在“同步、性能低下”的担忧。</strong>因此,我们可以确定如下几点:</p>
<ol>
<li>标准三剑客是文件描述符,任何基于文件的读写库函数都可以向标准输出和标准错误写入。</li>
<li>向标准输出和标准错误写入不影响日志框架的使用。</li>
<li>即便日志框架不提供缓冲区,也是可以提供一个实现了缓冲的 writer 以实现异步写入。</li>
</ol>
<p>那么,是否就可以由此断定在容器中将日志写入标准输出与标准错误跟写入文件相比就没有差别了呢?当然不是的,这要取决于标准输出与标准错误出重定向之目的地的写入能力,我们不妨拿<code>logback</code>来做个测试,<code>logback</code>是 java 领域应用广泛的日志框架,其往标准输出写入和文件写入分别是由<code>console</code>和<code>file</code>两个 appender 实现的,所以,我们首先在写入目的地的写入能力相同的情况下测试两个 appender 的能力是否有所差异,方法就是在<code>console</code>输出时重定向到文件,这样实际的 I/O 就都是普通文件 I/O 了。</p>
<table>
<thead>
<tr>
<th>写入条数</th>
<th>console</th>
<th>file</th>
<th>写入大小</th>
</tr>
</thead>
<tbody><tr>
<td>1000000</td>
<td>1729ms</td>
<td>1771ms</td>
<td>85m</td>
</tr>
<tr>
<td>5000000</td>
<td>7414ms</td>
<td>7708ms</td>
<td>480m</td>
</tr>
<tr>
<td>10000000</td>
<td>14545ms</td>
<td>15854ms</td>
<td>800m</td>
</tr>
<tr>
<td>20000000</td>
<td>32030ms</td>
<td>31754ms</td>
<td>1.7g</td>
</tr>
<tr>
<td>40000000</td>
<td>59479ms</td>
<td>59339ms</td>
<td>3.4g</td>
</tr>
</tbody></table>
<p>简单粗暴地将类似的内容以不同的条数写入,观察总的执行时间。由上述表格可知,这两个<code>appender</code>的自身能力可以说基本相同。在此基础上可以放在 k8s 上运行了,我这里仅对<code>40000000</code>的情况做一下测试,注意不要和上面表格对比,上面是 nvme 硬盘的响应时间,接下来的测试是普通企业 sas 盘的响应时间:</p>
<table>
<thead>
<tr>
<th>写入条数</th>
<th>console</th>
<th>file</th>
<th>写入大小</th>
</tr>
</thead>
<tbody><tr>
<td>40000000</td>
<td>111778ms</td>
<td>82679ms</td>
<td>3.4g</td>
</tr>
<tr>
<td>40000000</td>
<td>100143ms</td>
<td>87610ms</td>
<td>3.4g</td>
</tr>
<tr>
<td>40000000</td>
<td>98589ms</td>
<td>91603ms</td>
<td>3.4g</td>
</tr>
<tr>
<td>40000000</td>
<td>97093ms</td>
<td>91193ms</td>
<td>3.4g</td>
</tr>
<tr>
<td>40000000</td>
<td>98536ms</td>
<td>86348ms</td>
<td>3.4g</td>
</tr>
</tbody></table>
<p>仅就这单一的测试场景而论,使用<code>console appender</code>的性能会略逊一筹,大约有 10% 左右的性能损失,产生这种结果的原因是由容器中标准输出的目的地不同于文件造成的。</p>
<p>k8s 中的容器,除了使用<code>exec</code>附加到容器 namespace 启动的进程有控制终端之外,大部分以后台进程运行的程序是没有控制终端的,不妨进入容器的 namespace 看看服务进程的标准三剑客指向哪里:</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">/ # ps aux</span><br><span class="line">PID USER TIME COMMAND</span><br><span class="line"> 1 root 0:06 /usr/local/bin/isatty</span><br><span class="line"> 7 root 0:00 sh</span><br><span class="line"> 13 root 0:00 ps aux</span><br><span class="line">/ # cd /proc/1/fd</span><br><span class="line">/proc/1/fd # ls -l</span><br><span class="line">total 0</span><br><span class="line">lrwx------ 1 root root 64 Dec 12 02:25 0 -> /dev/null</span><br><span class="line">l-wx------ 1 root root 64 Dec 12 02:25 1 -> pipe:[54791]</span><br><span class="line">l-wx------ 1 root root 64 Dec 12 02:25 2 -> pipe:[54792]</span><br></pre></td></tr></table></figure>
<p>1 号进程是我们的测试进程,可见其标准输入指向<code>/dev/null</code>,标准输出和标准错误分别指向不同的管道,不知你是否好奇这是如何做到的呢?容器的日志又是如何写到<code>/var/log/containers</code>中的呢?</p>
<p><em><strong>参考文献</strong></em></p>
<ol>
<li><span class="exturl" data-url="aHR0cHM6Ly9ib29rLmRvdWJhbi5jb20vc3ViamVjdC8yNTkwMDQwMy8=">UNIX环境高级编程<i class="fa fa-external-link-alt"></i></span></li>
<li><span class="exturl" data-url="aHR0cHM6Ly9ib29rLmRvdWJhbi5jb20vc3ViamVjdC8yNTgwOTMzMC8=">Linux/UNIX系统编程手册<i class="fa fa-external-link-alt"></i></span></li>
</ol>
</div>
<footer class="post-footer">
<div class="post-eof"></div>
</footer>
</article>
</div>
<div class="post-block">
<article itemscope itemtype="http://schema.org/Article" class="post-content" lang="">
<link itemprop="mainEntityOfPage" href="http://liupzmin.com/2023/11/22/theory/terminal-fun-fact/">
<span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
<meta itemprop="image" content="/images/gzh.jpg">
<meta itemprop="name" content="巴流">
</span>
<span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
<meta itemprop="name" content="兔子先生">
<meta itemprop="description" content="左手人文 | 右手科技">
</span>
<span hidden itemprop="post" itemscope itemtype="http://schema.org/CreativeWork">
<meta itemprop="name" content="undefined | 兔子先生">
<meta itemprop="description" content="">
</span>
<header class="post-header">
<h2 class="post-title" itemprop="name headline">
<a href="/2023/11/22/theory/terminal-fun-fact/" class="post-title-link" itemprop="url">终端闲思录(1)- 世界是我的表象</a>
</h2>
<div class="post-meta-container">
<div class="post-meta">
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="far fa-calendar"></i>
</span>
<span class="post-meta-item-text">发表于</span>
<time title="创建时间:2023-11-22 20:23:00" itemprop="dateCreated datePublished" datetime="2023-11-22T20:23:00+08:00">2023-11-22</time>
</span>
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="far fa-calendar-check"></i>
</span>
<span class="post-meta-item-text">更新于</span>
<time title="修改时间:2024-08-02 12:16:16" itemprop="dateModified" datetime="2024-08-02T12:16:16+08:00">2024-08-02</time>
</span>
<span class="post-meta-item">
<span class="post-meta-item-icon">
<i class="far fa-folder"></i>
</span>
<span class="post-meta-item-text">分类于</span>
<span itemprop="about" itemscope itemtype="http://schema.org/Thing">
<a href="/categories/terminal/" itemprop="url" rel="index"><span itemprop="name">terminal</span></a>
</span>
,
<span itemprop="about" itemscope itemtype="http://schema.org/Thing">
<a href="/categories/terminal/computer-theory/" itemprop="url" rel="index"><span itemprop="name">computer theory</span></a>
</span>
</span>
</div>
</div>
</header>
<div class="post-body" itemprop="articleBody">
<p><strong>终端</strong>是我们习焉不察,日用而不知的一种工具,如果去问一个 Linux 爱好者:“Linux 中最神秘的东西是什么?” 我相信回答<strong>“终端”</strong>的人一定不在少数。<strong>黑洞洞的窗口,像音符一样跳动的命令与输出,适时闪烁的光标,无一不弥漫着古老而又神秘的气息!</strong></p>
<p><img data-src="https://qiniu.liupzmin.com/ssh.jpg" alt="黑客帝国中的终端"></p>
<h2 id="1-日志联想"><a href="#1-日志联想" class="headerlink" title="1 日志联想"></a>1 日志联想</h2><p>促使我 dig 终端的原因是我所在的公司要上线一个新项目,要求采用 k8s 作为运行平台。那么,日志处理方面就需要一个合理的方案。</p>
<p>我注意到 k8s 官方给出了几个<span class="exturl" data-url="aHR0cHM6Ly9rdWJlcm5ldGVzLmlvL3poLWNuL2RvY3MvY29uY2VwdHMvY2x1c3Rlci1hZG1pbmlzdHJhdGlvbi9sb2dnaW5nLw==">可行方案<i class="fa fa-external-link-alt"></i></span>:</p>
<ul>
<li><span class="exturl" data-url="aHR0cHM6Ly9rdWJlcm5ldGVzLmlvL3poLWNuL2RvY3MvY29uY2VwdHMvY2x1c3Rlci1hZG1pbmlzdHJhdGlvbi9sb2dnaW5nLyN1c2luZy1hLW5vZGUtbG9nZ2luZy1hZ2VudA==">使用节点级日志代理<i class="fa fa-external-link-alt"></i></span></li>
<li><span class="exturl" data-url="aHR0cHM6Ly9rdWJlcm5ldGVzLmlvL3poLWNuL2RvY3MvY29uY2VwdHMvY2x1c3Rlci1hZG1pbmlzdHJhdGlvbi9sb2dnaW5nLyNzaWRlY2FyLWNvbnRhaW5lci13aXRoLWxvZ2dpbmctYWdlbnQ=">使用边车容器运行日志代理<i class="fa fa-external-link-alt"></i></span></li>
<li><span class="exturl" data-url="aHR0cHM6Ly9rdWJlcm5ldGVzLmlvL3poLWNuL2RvY3MvY29uY2VwdHMvY2x1c3Rlci1hZG1pbmlzdHJhdGlvbi9sb2dnaW5nLyMlRTUlODUlQjclRTYlOUMlODklRTYlOTclQTUlRTUlQkYlOTclRTQlQkIlQTMlRTclOTAlODYlRTUlOEElOUYlRTglODMlQkQlRTclOUElODQlRTglQkUlQjklRTglQkQlQTYlRTUlQUUlQjklRTUlOTklQTg=">具有日志代理功能的边车容器<i class="fa fa-external-link-alt"></i></span></li>
<li><span class="exturl" data-url="aHR0cHM6Ly9rdWJlcm5ldGVzLmlvL3poLWNuL2RvY3MvY29uY2VwdHMvY2x1c3Rlci1hZG1pbmlzdHJhdGlvbi9sb2dnaW5nLyNleHBvc2luZy1sb2dzLWRpcmVjdGx5LWZyb20tdGhlLWFwcGxpY2F0aW9u">从应用中直接暴露日志目录<i class="fa fa-external-link-alt"></i></span></li>
</ul>
<p>其中,前两个方案都要求应用或者边车将日志写入标准输出和标准错误,相应的容器运行时负责将其转储为文件,最后由节点级的日志代理统一收集整理。后面两个方案其资源成本和开发成本都比较可观,并且将无法使用<code>kubectl logs</code>访问日志。</p>
<p>很明显可以看出,k8s 是偏向于应用将日志写入标准输出和标准错误的,这让我想起一位架构师朋友曾经说:<strong>“我不想把日志打到控制台,因为控制台是同步的,这会影响性能。”</strong></p>
<p>这段话即便不是错误的,至少也是不准确的。鉴于长久以来都对<strong>终端、控制台、标准输入与标准输出以及标准错误</strong>(Unix 世界竟然没有一个简单的概念来统称这三个文件描述符,为了方便称呼,后续我将在这三者同时出现的地方以<strong>标准三剑客</strong>来代替)等概念笼统对待,为破除这一刻板印象,并理顺 k8s 中处理日志的思路,搞清楚打印到标准输出与标准错误是否合理,我做了一些研究工作,清理了如下三个障碍:</p>
<ol>
<li>终端与标准三剑客的关系</li>
<li>标准三剑客与缓冲</li>
<li>k8s 是如何重定向标准三剑客的 ?</li>
</ol>
<p>这些问题真的那么重要吗?不妨假想一下:</p>
<blockquote>
<p>k8s 建议你将日志打印到标准输出和标准错误,而你是一个略微有点计算机文化积淀的人,你知道在 C 标准库里标准三剑客的 I/O 缓冲各不相同,缓冲最大的也就是个行缓冲,你会狐疑着说:"我都不确定我所用语言的缓冲设计,我能放心地往标准输出和标准错误写吗?你把那些优秀的高吞吐日志框架至于何地?"</p>
<p>这时,一位大腹便便的中年人慢悠悠踱到你面前,语重心长地说道:“少侠,稍安勿躁,谁说往标准输出和标准错误写就一定写往终端 ?就一定是行缓冲 ?就一定要用<strong>‘printf’</strong>、<strong>‘fmt.Print’</strong>、<strong>‘system.out.println’</strong> ?”</p>
<p>一连串的反问让你内心掠过一丝不快,但望着他稀疏的额头和有些混浊却不失坚毅的眼睛,你最终只是微微张了张嘴,没能说出一个字。</p>
<p>他肥胖的身躯坐了下来,用手捋着下巴上一撮小胡须,施施然道:“闻道有先后,不知道并不可怕,一知半解才可怕,我看你方才只是‘狐疑’着发问,并未理直气壮、斩钉截铁、目空一切,而且也不曾顶撞老夫,说明孺子可教,我就拣重点与你说说吧!”</p>
<p>说罢,便开始了涛涛雄辩。</p>
</blockquote>
<p>言归正传,我将用三篇文章来把这三个问题讲清楚,这是系列第一篇,先让我们进入终端的神秘世界吧!</p>
<h2 id="2-终端迷雾"><a href="#2-终端迷雾" class="headerlink" title="2 终端迷雾"></a>2 终端迷雾</h2><p>终端(terminal),源自拉丁语 <em>terminalis</em>,意为“与边界或结尾有关,最终的”,"与计算机通信的设备"之意首次记录于1954年,时间上距今不足百年,而计算机日新月异的发展速度使得很多事物快速出现又快速消亡,变成随时间层层累积的沉积岩,搞清楚其源流嬗变殊为不易,以至于后来的我们很难看清事情的原貌。我实在很想拥有钩沉索微的能力,去近距离感受每处痕迹背后的波澜壮阔,而不是如今只能通过抚摸巨岩的横截面,来想象那个时代的风云际会。</p>
<p>终端是一种和计算机交互的硬件设备(早期是硬件,如今已是软件),用于处理输入和输出。最早的硬件终端是电传打印机(<strong>teleprinter</strong>、<strong>teletypewriter</strong>、<strong>teletype</strong>、<strong>TTY</strong> 指的都是电传打印机),显示内容需要打印到纸上,这也是为什么我们在编程中向终端打印使用 <strong>print</strong> 而不是 <strong>display</strong> 的原因,使用屏幕显示内容还要等到 CRT(阴极射线管)设备的出现。</p>
<p><img data-src="https://qiniu.liupzmin.com/teletype.jpg" alt="图 2-1 二战时期的电传打印机"></p>
<p>这些硬件终端与计算机通过串口直连,或者通过调制解调器远程连接,不过这种连接方式的距离和终端数量都很有限。如果把计算机比作一条章鱼,那么终端就是触手的顶端,从拉丁词源 <em>terminalis</em> “与边界或结尾有关,最终的”之意中,我们多少还能窥见这一层含义。</p>
<p><img data-src="https://qiniu.liupzmin.com/DEC_VT100_mid.jpg" alt="图 2-2 图形终端 VT100"></p>
<p>图 2-2 是 DEC 公司生产的图形终端 <strong>VT-100</strong> ,广泛流行的终端模拟器和 SSH 客户端软件 SecureCRT 中,可以设置模拟的终端类型,其中就有 VT-100 系列。从 SecureCRT 各种终端类型中,依然可以看出当年终端设备市场是怎样一个山头林立的状态,这些设备没有统一的标准,各自有各自的字符转义序列。所谓的字符转义序列是指向终端发送的特殊控制字符,终端会将这些特殊字符解释为相应的功能,比如调整终端的显示,vi 类软件特别需要标准化,再比如<code>Ctrl+C</code>是向会话中的前台进程发送<code>SIGINT</code>信号(终端如何得知哪一个是前台进程请参考 Unix/Linux 手册进程组部分的内容),<code>Ctrl+D</code>会使得从终端读取输入的进程读取到一个<code>end-of-file</code>。</p>
<p>现代意义上的终端已经几乎全部虚拟化、软件化了,Unix/Linux 系统可以通过<code>Ctrl+Alt+Fn</code> 组合键切换虚拟终端,现代 Linux 系统通常在其中一个终端启动图形界面,我的 Manjaro 桌面就启动在 <code>F2</code> 上。Unix/Linux 在功能键上启动的这些终端即是虚拟终端<code>/dev/ttyn</code>。</p>
<p>另一个与终端有关的概念是 Console —— 控制台,控制台其实是一种终端,大多时候和计算机长在一起,不一定要有屏幕,摇杆、按钮也可称为控制台,只要是能控制计算机的都属于控制台。</p>
<p>现代个人计算机已经没有控制台了,终端和控制台已经虚拟化,且大多时候混用这两概念也没有问题。但寻其本意依然都有处理计算机输入与输出的意思,又因终端和控制台经常和标准三剑客关联,有些软件不免就会混淆其中,例如 java 著名的日志框架 logback 有一个输出目的地 <strong>ConsoleAppender</strong> ,其实是用<em>System.out</em> 、 <em>System.err</em> 将内容写往标准输出和标准错误的,而标准输出和标准错误并不一定连接终端或控制台,标准三剑客的相关内容我会在后面详述,现在我们有必要看一下传统的终端登录过程,看看终端是如何被打开的。</p>
<p><img data-src="https://qiniu.liupzmin.com/bsd-terminal-login.png" alt="图 2-3 终端登录"><img data-src="https://qiniu.liupzmin.com/terminal-login-shell.png" alt="图 2-4 终端与shell关联"></p>
<p>SysV 系统中,init 进程是系统启动后用户空间拉起的第一个进程,也叫 1 号进程,现代 Linux 如果采用 Systemd 系统启动,其 1 号进程是 Systemd 进程。传统 Unix 在启动时 init 进程会扫描<code>/etc/ttys</code>中的内容,<code>/etc/ttys</code>中配置有连接到该计算机上的终端设备列表,init 进程会遍历每个设备,针对每个设备都 fork 一个进程来处理,图 2-3 展示了这个过程。</p>
<p>fork 之后子进程会执行<code>getty</code>程序,getty 会打开终端,如果终端是通过调制解调器连接的,getty 会等待对方拨号,一旦设备打开成功,文件描述符<code>0,1,2</code>就被设置到终端上了,而<code>0,1,2</code>就是标准三剑客,如果不出意外(文件描述符未被重定向),后续对<code>0,1,2</code>的读写都是对终端的读写,在内核中由<strong>终端驱动</strong>提供服务。不难想见,终端驱动会吸收键盘的输入,会将输出打印到设备屏幕。</p>
<p>getty 的最后一项使命是向终端打印<strong>“login:”</strong>,等待我们输入用户名,一旦我们输入用户名回车,getty 便功成身退,它会以类似如下方式调用<code>login</code>程序:</p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">execle(<span class="string">"/bin/login"</span>, <span class="string">"login"</span>, <span class="string">"-p"</span>, username, (<span class="type">char</span> *)<span class="number">0</span>, envp);</span><br></pre></td></tr></table></figure>
<p>login 程序会向终端打印<strong>“Password:”</strong>来提示用户输入密码,当然终端的回显功能会被关掉。接下来 login 会做一系列的工作,比如鉴权、设置环境变量、设置 HOME 目录、开启会话、设置进程组等等,最后会调用 shell 程序:</p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">execl(<span class="string">"/bin/sh"</span>, <span class="string">"-sh"</span>, (<span class="type">char</span> *)<span class="number">0</span>);</span><br></pre></td></tr></table></figure>
<p>如图 2-4 所示,execl 后进程变为 shell,但<code>0,1,2</code>文件描述符得以保留,毋庸置疑的是,此时三剑客指向终端,shell 进程的读写都依赖终端驱动程序处理。我们现在将终端驱动部分放大,看看其内部对输入输出的处理过程,如图 2-5 所示:</p>
<p><img data-src="https://qiniu.liupzmin.com/terminal-io.png" alt="图 2-5 终端设备的输入输出序列"></p>
<p>进程对于终端设备的读写由终端驱动处理,终端驱动维护着两个队列:<strong>输入队列和输出队列</strong>,键盘的输入会进入输入队列,最后被进程读取;进程的输出会进入输出队列,由终端显示。如果该终端设备的回显功能被打开,进入输入队列的内容会同时发送到输出队列,由终端设备显示在屏幕上,这就是你敲击键盘后终端中显示内容的原因,前文的 login 程序在收集密码时就会将回显功能关闭。</p>
<p>我之所以讲“你敲击键盘后终端中显示内容”而不是“shell 中显示内容”,是为了强调一个事实:<strong>你能看到的所有内容都是你的输入和shell 的输出</strong>。你永远不会在 shell 里面,你只能给它提供输入,观察它的输出,当然是在终端上。</p>
<p>而此刻,shell 向终端输出了<code>root@hostname:~ #</code>的提示符,终端驱动程序将其打印到可见终端,当然,这个终端有可能是<code>Ctrl+Alt+Fn</code> 组合键下显示器呈现的虚拟终端,也有可能是视窗界面下的终端模拟器,但基本不可能是最初的终端设备了。</p>
<p>这当然是因为那些设备现在已经消失了,物理设备一经消亡,原本显示的重任就交给显卡来处理了,终端驱动想必会因此生出黍离一样的悲痛。而我们当然不会在乎终端驱动是否悲痛,我们只是对这种割裂感有所不适。</p>
<p><code>Ctrl+Alt+Fn</code> 组合键大概是对早期硬件终端连接场景的模拟,毕竟在 PC 个人化以及 shell 作业控制广泛应用的今天,实在看不出它还有什么其它的意义。因为在这种场景下进入终端,我们只能看到显示设备(不能称之为终端,如显示器)以及 shell 的输出,终端一词就像无源之水、无根之木一样没了着落。</p>
<p>请允许我用夸张的舞台腔背诵加缪《西西弗神话》中的段落:"在一个突然被剥夺了幻觉和光明的宇宙中,人就感到自己是个局外人。这种放逐无可救药,因为人被剥夺了对故乡的回忆和对乐土的希望。这种人和生活的分离,演员和布景的分离,正是荒诞感。"</p>
<p>终端和 shell 的概念纠缠正是出于这种荒诞感!</p>
<p>讲到这里,似乎不再需要刻意去辨析终端和 shell 的区别了,现象已经很明朗:连接到终端的最终程序是 shell,shell 以及由此 shell 启动的任何程序其三剑客都指向终端,它们的输出也会打印到终端。作为计算机行业的新新人类,失去了物理真实的触摸,极目所望,尽是 shell 及其子孙的输出,终端的概念早已淹没其中,混淆也就在所难免了。</p>
<p>视窗界面下的终端模拟器似乎将这种荒诞缓和了一些,但也仅仅是一些,在一个单薄的窗口中,shell 的输出占据了绝大多数的领地,只有边框和工具栏在隐隐的提示人们:我是有形的!</p>
<p>无论如何,由电传打印机沿袭下来的 <code>tty</code> 一词却在 Unix 中留下了深深的印记,tty 子系统、tty 驱动、虚拟终端 /dev/ttyn、伪终端 pty 等都有着<code>teletype</code>的影子。</p>
<h2 id="3-大行其道的伪终端"><a href="#3-大行其道的伪终端" class="headerlink" title="3 大行其道的伪终端"></a>3 大行其道的伪终端</h2><p>第 2 节介绍的终端登录场景,进程打开的设备是<code>/dev/ttyn</code>,通常被称为虚拟终端,基本只有在<code>Ctrl+Alt+Fn</code> 组合键和虚拟机管理界面的控制台进入的终端属于此类。大多数场景用的都是伪终端,比如终端模拟器和网络 SSH 登录,本节我们就梳理一下这两种常见的终端场景。</p>
<p>伪终端其实是 IPC(进程间通信)的一种,它有一对主从设备, 也叫伪终端对,分别连接着两个进程:</p>
<p><img data-src="https://qiniu.liupzmin.com/pseudo.png" alt="图 3-1 使用伪终端的相关进程的典型结构"></p>
<p>图 3-1 是使用伪终端相关进程的典型结构,伪终端主设备和从设备组成了一个双向管道,连接了两个进程。通常连接从设备的进程是 shell,所以,对 shell 来讲伪终端从设备表现的就像原来的终端设备,终端驱动也是和从设备相连,进程对终端的读写都发往从设备。</p>
<p>与以往不同的是,进程眼中的终端设备在这里不以显示为直接目的,而是将输出发往另一个进程,输入也要从另一个进程读取,而这正是为终端模拟器和网络 SSH 登录设计的,我们先看一看终端模拟器的情况:</p>
<p><img data-src="https://qiniu.liupzmin.com/terminal-emulator.png" alt="图 3-2 使用伪终端的终端模拟器"></p>
<p>图 3-2 展示了一个打开了两个窗口的终端模拟器,终端模拟器是一个图形化的视窗程序,针对每一个窗口创建一个伪终端对,并 fork 出 shell 进程,将 shell 进程的标准三剑客连接到伪终端从设备,如此一来,shell 便从终端模拟器程序读取输入,输出发往终端模拟器,最后被渲染到窗口界面。这是连接本地终端的情况,下面再看一下 SSH 登录:</p>
<p><img data-src="https://qiniu.liupzmin.com/ssh-pseudo.png" alt="图 3-3 使用伪终端的 SSH"></p>
<p>SSH 是使用伪终端的另一个例子,它允许本地用户安全地通过网络连接到远程机器上登录 shell,图 3-3 展示了这种情况,ssh server 为每个登录请求创建伪终端对,并 fork 出 shell 进程连接到伪终端从设备。客户端的输入通过网络抵达 ssh server,ssh server 发往伪终端主设备,最终变为 shell 进程的标准输入;同样,由 shell 产生的输出经过伪终端主设备抵达 ssh server,再经 ssh server 发送到网络,最终被 ssh client 接收。现在请你思考一下:ssh client 会如何处理接收到的 shell 输出呢?</p>
<p>从图中不难看出,ssh client 要将 shell 的输出送往终端显示,问题是你能猜出图中的<code>terminal</code>是什么设备吗?</p>
<p>其实我们已经讲过了,此处的<code>terminal</code>可以看作图 3-2 的缩影,ssh client 连接的是本地伪终端对中的从设备,用户使用的可能是终端模拟器,模拟器 fork 的进程就是 ssh client,shell 的提示符<code>root@localhost:~ # </code>历经千山万水,终于呈现在你本地的终端模拟器上了。</p>
<h2 id="4-标准三剑客的本质"><a href="#4-标准三剑客的本质" class="headerlink" title="4 标准三剑客的本质"></a>4 标准三剑客的本质</h2><p>Linux 会为打开的文件分配一个非负整数来表示该文件,文件的 I/O 调用都要通过文件描述符来发起,文件描述符用来表示所有类型的已打开的文件,这包括管道(pipe)、FIFO、socket、普通文件和终端设备等。Linux 为这些类型的文件提供了统一的通用 I/O 模型,即 open、close、read、write 等系统调用接口,因此,所谓的<strong>“Linux 一切皆文件”</strong>应该更多地从通用文件 I/O 接口的角度来理解。</p>
<p>我们所讨论的终端即是其中一种文件类型,标准三剑客表示的<strong>“0,1,2”</strong>三个文件描述符,背后的文件类型通常是终端设备,例外情况等我讲到复制文件描述符的时候再详细讨论,我们先明确一下文件描述符和文件的对应关系。</p>
<p>打开文件,获得文件的描述符,似乎文件和文件描述符的一对一关系是不言而喻的,但是,<strong>多个文件描述符可以指向同一打开的文件,这些文件描述符可以在相同或不同的进程打开。</strong>如图 4-1 所示:</p>
<p><img data-src="https://qiniu.liupzmin.com/file-fd-releationship.png" alt="图 4-1 文件描述符,打开的文件描述,文件inode之间的关系"></p>
<p>上图展示了进程的文件描述符(file descriptor)、内核维护的系统所有打开的文件描述(open file description)以及文件 inode 之间的关系。简单介绍一下,左侧表格代表进程的文件描述符;中间表格称为 open file description table,是内核为所有打开的文件维护的一个系统级描述表;右侧表格代表 inode,可简单理解为硬盘上的文件。</p>
<p>从中我们可以得到以下几点信息:</p>
<ol>
<li>在进程 A 中,<strong>文件描述符</strong> 1 和 20 都指向同一个<strong>打开文件描述</strong>,这可能是通过复制文件描述符形成的。</li>
<li>进程 A 的文件描述符 2 和 进程 B 的文件描述符 2 都指向同一个<strong>打开文件描述</strong>,这种情形可能是进程 A 进行 fork 调用形成的,子进程会继承父进程所有打开的文件描述符。</li>
<li>进程 A 的文件描述符 0 和进程 B 的文件描述符 3 分别指向不同的<strong>打开文件描述</strong>,但这些描述均指向相同的 inode,这是因为每个进程各自对同一文件发起了 open 调用,同一进程两次打开同一文件也会出现这种情况。</li>
</ol>
<p>关于第一点复制文件描述符稍后另行展开,我们先看第二点,父进程 fork 出子进程,子进程会继承父进程所有打开的文件描述符,如果子进程稍后调用 exec 执行了其它的程序,那些没有设置<code>O_CLOEXEC</code>的文件描述符都会在子进程中得到保留。</p>
<p>shell 进程已经打开了<strong>“0,1,2”</strong>三个文件描述符,在此shell中执行的所有进程都会继承这三个文件描述符,如果没有特殊的变动,它们会和 shell 一样将<strong>“0,1,2”</strong>连接到终端。你是否有过这样的经历:在 shell 中执行了程序,程序进入了后台,回车后可见 shell 的提示符,说明你依然可以操作,但是进入后台的程序却时不时的输出一点内容到你的终端,如果你没遇到过,可以用下面的命令试一下:</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">for ((;;)) do sleep 1; echo "hello"; done &</span><br></pre></td></tr></table></figure>
<p>如果你需要终止它,输入<code>fg</code>将进程拉到前台,<code>Ctrl+C</code>结束它,不要害怕<code>f</code>和<code>g</code>之间被<code>hello</code>充塞,世界只是你的表象,表象虽然乱了,但内在的输入队列和输出队列依然有序运行。</p>
<p>之所以产生这种现象是因为没有为放到后台的进程处理标准输出和标准错误, shell fork 子进程出来解释该命令,子进程继承了 <strong>“0,1,2”</strong>文件描述符,<code>&</code>标志将进程送往后台,但是并没有对标准三剑客进行调整,其对应的<strong>“0,1,2”</strong>描述符依然和终端相关联,所以当<code>echo "hello"</code>向标准输出打印的时候,内容依然会显示在终端上。不过不用担心后台进程会干扰标准输入,shell 会确保只有前台进程才能从终端进行读取(参考进程组的内容)。</p>
<p>程序并不经常产生这种行为,大多数情况下我们需要在 shell 中通过重定向语法<strong>“>”</strong>来处理标准三剑客。有些支持 daemon 的程序会提供诸如<code>-d</code>或<code>--detach</code>的选项在处理好标准三剑客之后启动到后台,一种典型的处理是将标准三剑客指向<strong>“/dev/null”</strong>,因为 daemon 程序通常并不需要使用终端;而 systemd 管理下的 service 通常会将标准输出和标准错误重定向到 Unix 域套接字,这些输出内容作为日志受到 journald 进程的管理。</p>
<p>完成这种重定向的就是 dup 家族的 <strong>dup</strong>、<strong>dup2</strong>、<strong>dup3</strong> 三个系统调用。使用最多的是 dup2:</p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><unistd.h></span></span></span><br><span class="line"><span class="type">int</span> <span class="title function_">dup2</span><span class="params">(<span class="type">int</span> oldfd, <span class="type">int</span> newfd)</span>;</span><br><span class="line"><span class="comment">//Returns (new) file descriptor on success, or –1 on error</span></span><br></pre></td></tr></table></figure>
<p>dup2() 系统调用会为 oldfd 参数指定的文件描述符创建副本,副本的编号由 newfd 参数指定,所以<code>dup(1, 20)</code>就会产生图 4-1 中所示进程 A 的情况。shell 的重定向语法和管道就是使用 dup 实现的。</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">./myscript > results.log 2>&1</span></span><br></pre></td></tr></table></figure>
<p>上面这条重定向命令被广泛使用,Bourne shell 的重定向语法<strong>“2>&1”</strong>,意在通知 shell 把标准错误重定向到标准输出,这条语法的效果大致使用如下方式实现:</p>
<figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">fd = open(<span class="string">"results.log"</span>, O_RDWR);</span><br><span class="line"><span class="keyword">if</span> (dup2(fd, STDOUT_FILENO) != STDOUT_FILENO)</span><br><span class="line"> <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line"><span class="keyword">if</span> (dup2(STDOUT_FILENO, STDERR_FILENO) != STDERR_FILENO)</span><br><span class="line"> <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line"><span class="comment">// 文件描述符复制完毕,fd 可以关闭</span></span><br><span class="line">close(fd);</span><br></pre></td></tr></table></figure>
<p>这一刻,<strong>STDOUT_FILENO</strong> 和 <strong>STDERR_FILENO</strong> ,也就是文件描述符<strong>“1,2”</strong>与终端脱离关系,写往标准输出和标准错误的内容全部进入 results.log 文件,不会再显示在终端上。</p>
<blockquote>
<p>所以,<strong>标准三剑客的本质仅仅是文件描述符</strong>,各种语言中那些能打印到终端的 I/O 函数(如<strong>‘printf’</strong>、<strong>‘fmt.Print’</strong>、<strong>‘system.out.println’</strong>),其底层使用的就是<strong>“0,1,2”</strong>三个文件描述符,函数最终输出到哪里要看<strong>“0,1,2”</strong>指向哪里。</p>
</blockquote>
<p>还记得<strong>“Linux 一切皆文件”</strong>指得是通用文件 I/O 接口吗?文件描述符可以指向任意类型的文件,我们再来看一个管道的例子:</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ls | wc -l</span><br></pre></td></tr></table></figure>