-
Notifications
You must be signed in to change notification settings - Fork 154
/
Copy path2.5.html
1632 lines (1116 loc) · 182 KB
/
2.5.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="" >
<head>
<meta charset="UTF-8">
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<title>输入输出 · GitBook</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="description" content="">
<meta name="generator" content="GitBook 3.2.3">
<link rel="stylesheet" href="gitbook/style.css">
<link rel="stylesheet" href="gitbook/gitbook-plugin-intopic-toc/style.css">
<link rel="stylesheet" href="gitbook/gitbook-plugin-page-footer-ex/style/plugin.css">
<link rel="stylesheet" href="gitbook/gitbook-plugin-callouts/plugin.css">
<link rel="stylesheet" href="gitbook/gitbook-plugin-highlight/website.css">
<link rel="stylesheet" href="gitbook/gitbook-plugin-search/search.css">
<link rel="stylesheet" href="gitbook/gitbook-plugin-fontsettings/website.css">
<link rel="stylesheet" href="gitbook/gitbook-plugin-theme-comscore/test.css">
<link rel="stylesheet" href="styles.css">
<meta name="HandheldFriendly" content="true"/>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="gitbook/images/apple-touch-icon-precomposed-152.png">
<link rel="shortcut icon" href="gitbook/images/favicon.ico" type="image/x-icon">
<link rel="next" href="PA3.html" />
<link rel="prev" href="2.4.html" />
</head>
<body>
<div class="book">
<div class="book-summary">
<div id="book-search-input" role="search">
<input type="text" placeholder="Type to search" />
</div>
<nav role="navigation">
<ul class="summary">
<li class="chapter " data-level="1.1" data-path="index.html">
<a href="index.html">
Introduction
</a>
</li>
<li class="chapter " data-level="1.2" data-path="PA0.html">
<a href="PA0.html">
PA0 - 世界诞生的前夜: 开发环境配置
</a>
<ul class="articles">
<li class="chapter " data-level="1.2.1" data-path="0.1.html">
<a href="0.1.html">
Installing GNU/Linux
</a>
</li>
<li class="chapter " data-level="1.2.2" data-path="0.2.html">
<a href="0.2.html">
First Exploration with GNU/Linux
</a>
</li>
<li class="chapter " data-level="1.2.3" data-path="0.3.html">
<a href="0.3.html">
Installing Tools
</a>
</li>
<li class="chapter " data-level="1.2.4" data-path="0.4.html">
<a href="0.4.html">
Configuring vim
</a>
</li>
<li class="chapter " data-level="1.2.5" data-path="0.5.html">
<a href="0.5.html">
More Exploration
</a>
</li>
<li class="chapter " data-level="1.2.6" data-path="0.6.html">
<a href="0.6.html">
Getting Source Code for PAs
</a>
</li>
</ul>
</li>
<li class="chapter " data-level="1.3" data-path="PA1.html">
<a href="PA1.html">
PA1 - 开天辟地的篇章: 最简单的计算机
</a>
<ul class="articles">
<li class="chapter " data-level="1.3.1" data-path="1.1.html">
<a href="1.1.html">
在开始愉快的PA之旅之前
</a>
</li>
<li class="chapter " data-level="1.3.2" data-path="1.2.html">
<a href="1.2.html">
开天辟地的篇章
</a>
</li>
<li class="chapter " data-level="1.3.3" data-path="1.3.html">
<a href="1.3.html">
RTFSC
</a>
</li>
<li class="chapter " data-level="1.3.4" data-path="1.4.html">
<a href="1.4.html">
基础设施
</a>
</li>
<li class="chapter " data-level="1.3.5" data-path="1.5.html">
<a href="1.5.html">
表达式求值
</a>
</li>
<li class="chapter " data-level="1.3.6" data-path="1.6.html">
<a href="1.6.html">
监视点
</a>
</li>
<li class="chapter " data-level="1.3.7" data-path="1.7.html">
<a href="1.7.html">
如何阅读手册
</a>
</li>
</ul>
</li>
<li class="chapter " data-level="1.4" data-path="PA2.html">
<a href="PA2.html">
PA2 - 简单复杂的机器: 冯诺依曼计算机系统
</a>
<ul class="articles">
<li class="chapter " data-level="1.4.1" data-path="2.1.html">
<a href="2.1.html">
不停计算的机器
</a>
</li>
<li class="chapter " data-level="1.4.2" data-path="2.2.html">
<a href="2.2.html">
RTFSC(2)
</a>
</li>
<li class="chapter " data-level="1.4.3" data-path="2.3.html">
<a href="2.3.html">
程序, 运行时环境与AM
</a>
</li>
<li class="chapter " data-level="1.4.4" data-path="2.4.html">
<a href="2.4.html">
基础设施(2)
</a>
</li>
<li class="chapter active" data-level="1.4.5" data-path="2.5.html">
<a href="2.5.html">
输入输出
</a>
</li>
</ul>
</li>
<li class="chapter " data-level="1.5" data-path="PA3.html">
<a href="PA3.html">
PA3 - 穿越时空的旅程: 批处理系统
</a>
<ul class="articles">
<li class="chapter " data-level="1.5.1" data-path="3.1.html">
<a href="3.1.html">
最简单的操作系统
</a>
</li>
<li class="chapter " data-level="1.5.2" data-path="3.2.html">
<a href="3.2.html">
穿越时空的旅程
</a>
</li>
<li class="chapter " data-level="1.5.3" data-path="3.3.html">
<a href="3.3.html">
用户程序和系统调用
</a>
</li>
<li class="chapter " data-level="1.5.4" data-path="3.4.html">
<a href="3.4.html">
文件系统
</a>
</li>
<li class="chapter " data-level="1.5.5" data-path="3.5.html">
<a href="3.5.html">
精彩纷呈的应用程序
</a>
</li>
</ul>
</li>
<li class="chapter " data-level="1.6" data-path="PA4.html">
<a href="PA4.html">
PA4 - 虚实交错的魔法: 分时多任务
</a>
<ul class="articles">
<li class="chapter " data-level="1.6.1" data-path="4.1.html">
<a href="4.1.html">
多道程序
</a>
</li>
<li class="chapter " data-level="1.6.2" data-path="4.2.html">
<a href="4.2.html">
虚实交错的魔法
</a>
</li>
<li class="chapter " data-level="1.6.3" data-path="4.3.html">
<a href="4.3.html">
超越容量的界限
</a>
</li>
<li class="chapter " data-level="1.6.4" data-path="4.4.html">
<a href="4.4.html">
来自外部的声音
</a>
</li>
<li class="chapter " data-level="1.6.5" data-path="4.5.html">
<a href="4.5.html">
编写不朽的传奇
</a>
</li>
</ul>
</li>
<li class="chapter " data-level="1.7" data-path="blank.html">
<a href="blank.html">
杂项
</a>
<ul class="articles">
<li class="chapter " data-level="1.7.1" data-path="FAQ.html">
<a href="FAQ.html">
常见问题(FAQ)
</a>
</li>
<li class="chapter " data-level="1.7.2" data-path="why.html">
<a href="why.html">
为什么要学习计算机系统基础
</a>
</li>
<li class="chapter " data-level="1.7.3" data-path="linux.html">
<a href="linux.html">
Linux入门教程
</a>
</li>
<li class="chapter " data-level="1.7.4" data-path="man.html">
<a href="man.html">
man入门教程
</a>
</li>
<li class="chapter " data-level="1.7.5" data-path="git.html">
<a href="git.html">
git入门教程
</a>
</li>
<li class="chapter " data-level="1.7.6" data-path="nemu-isa-api.html">
<a href="nemu-isa-api.html">
NEMU ISA相关API说明文档
</a>
</li>
<li class="chapter " data-level="1.7.7" data-path="changelog.html">
<a href="changelog.html">
更新日志
</a>
</li>
<li class="chapter " data-level="1.7.8" data-path="i386-intro.html">
<a href="i386-intro.html">
i386手册指令集阅读指南
</a>
</li>
<li class="chapter " data-level="1.7.9" data-path="exec.html">
<a href="exec.html">
指令执行例子
</a>
</li>
</ul>
</li>
<li class="divider"></li>
<li>
<a href="https://www.gitbook.com" target="blank" class="gitbook-link">
Published with GitBook
</a>
</li>
</ul>
</nav>
</div>
<div class="book-body">
<div class="body-inner">
<div class="book-header" role="navigation">
<!-- Title -->
<h1>
<i class="fa fa-circle-o-notch fa-spin"></i>
<a href="." >输入输出</a>
</h1>
</div>
<div class="page-wrapper" tabindex="-1" role="main">
<div class="page-inner">
<div id="book-search-results">
<div class="search-noresults">
<section class="normal markdown-section">
<h2 id="输入输出">输入输出</h2>
<p>我们已经成功运行了<code>cpu-tests</code>中的各个测试用例, 但这些测试用例都只能默默地进行纯粹的计算.
回想起我们在程序设计课上写的第一个程序<code>hello</code>, 至少也输出了一行信息.
事实上, 输入输出是计算机与外界交互的基本手段,
如果你还记得计算机刚启动时执行的BIOS程序的全称是Basic Input/Output System,
你就会理解输入输出对计算机来说是多么重要了.
在真实的计算机中, 输入输出都是通过访问I/O设备来完成的.</p>
<h3 id="设备与cpu">设备与CPU</h3>
<p>设备的工作原理其实没什么神秘的.
你会在不久的将来在数字电路实验中看到键盘控制器模块和VGA控制器模块相关的verilog代码.
噢, 原来这些设备也一样是个数字电路!
事实上, 只要向设备发送一些有意义的数字信号, 设备就会按照这些信号的含义来工作.
让一些信号来指导设备如何工作, 这不就像"程序的指令指导CPU如何工作"一样吗? 恰恰就是这样!
设备也有自己的状态寄存器(相当于CPU的寄存器), 也有自己的功能部件(相当于CPU的运算器).
当然不同的设备有不同的功能部件,
例如键盘有一个把按键的模拟信号转换成扫描码的部件,
而VGA则有一个把像素颜色信息转换成显示器模拟信号的部件.
控制设备工作的信号称为"命令字", 可以理解成"设备的指令",
设备的工作就是负责接收命令字, 并进行译码和执行...
你已经知道CPU的工作方式, 这一切对你来说都太熟悉了.</p>
<p>既然设备是用来进行输入输出的, 所谓的访问设备,
说白了就是从设备获取数据(输入), 比如从键盘控制器获取按键扫描码,
或者是向设备发送数据(输出), 比如向显存写入图像的颜色信息.
但是, 如果万一用户没有敲键盘, 或者是用户想调整屏幕的分辨率, 怎么办呢?
这说明, 除了纯粹的数据读写之外, 我们还需要对设备进行控制:
比如需要获取键盘控制器的状态, 查看当前是否有按键被按下;
或者是需要有方式可以查询或设置VGA控制器的分辨率.
所以, 在程序看来, 访问设备 = 读出数据 + 写入数据 + 控制状态.</p>
<p>我们希望计算机能够控制设备, 让设备做我们想要做的事情, 这一重任毫无悬念地落到了CPU身上.
CPU除了进行计算之外, 还需要访问设备, 与其协作来完成不同的任务.
那么在CPU看来, 这些行为究竟意味着什么呢?
具体要从哪里读数据? 把数据写入到哪里? 如何查询/设置设备的状态?
一个最本质的问题是, CPU和设备之间的接口, 究竟是什么?</p>
<p>答案也许比你想象中的简单很多: 既然设备也有寄存器,
一种最简单的方法就是把设备的寄存器作为接口, 让CPU来访问这些寄存器.
比如CPU可以从/往设备的数据寄存器中读出/写入数据, 进行数据的输入输出;
可以从设备的状态寄存器中读出设备的状态, 询问设备是否忙碌;
或者往设备的命令寄存器中写入命令字, 来修改设备的状态.</p>
<p>那么, CPU要如何访问设备寄存器呢?
我们先来回顾一下CPU是如何访问CPU自己的寄存器的:
首先给这些寄存器编个号, 比如<code>eax</code>是<code>0</code>, <code>ecx</code>是<code>1</code>...
然后在指令中引用这些编号, 电路上会有相应的选择器, 来选择相应的寄存器并进行读写.
对设备寄存器的访问也是类似的:
我们也可以给设备中允许CPU访问的寄存器逐一编号, 然后通过指令来引用这些编号.
设备中可能会有一些私有寄存器, 它们是由设备自己维护的,
它们没有这样的编号, CPU不能直接访问它们.</p>
<p>这就是所谓的I/O编址方式, 因此这些编号也称为设备的地址. 常用的编址方式有两种.</p>
<h3 id="端口io">端口I/O</h3>
<p>一种I/O编址方式是端口映射I/O(port-mapped I/O), CPU使用专门的I/O指令对设备进行访问,
并把设备的地址称作端口号.
有了端口号以后, 在I/O指令中给出端口号, 就知道要访问哪一个设备寄存器了.
市场上的计算机绝大多数都是IBM PC兼容机,
IBM PC兼容机对常见设备端口号的分配有<a href="https://wiki.osdev.org/I/O_Ports#The_list" target="_blank">专门的规定</a>.</p>
<p>x86提供了<code>in</code>和<code>out</code>指令用于访问设备,
其中<code>in</code>指令用于将设备寄存器中的数据传输到CPU寄存器中,
<code>out</code>指令用于将CPU寄存器中的数据传送到设备寄存器中.
一个例子是使用<code>out</code>指令给串口发送命令字:</p>
<pre><code class="lang-asm">movl $0x41, %al
movl $0x3f8, %edx
outb %al, (%dx)
</code></pre>
<p>上述代码把数据0x41传送到0x3f8号端口所对应的设备寄存器中.
CPU执行上述代码后, 会将0x41这个数据传送到串口的一个寄存器中,
串口接收之后, 发现是要输出一个字符<code>A</code>;
但对CPU来说, 它并不关心设备会怎么处理0x41这个数据, 只会老老实实地把0x41传送到0x3f8号端口.
事实上, 设备的API及其行为都会在相应的文档里面有清晰的定义,
在PA中我们无需了解这些细节, 只需要知道,
驱动开发者可以通过RTFM, 来编写相应程序来访问设备即可.</p>
<div class="panel panel-danger"><div class="panel-heading"><h5 class="panel-title" id="有没有一种熟悉的感觉"><i class="fa fa-bullhorn"></i> 有没有一种熟悉的感觉?</h5></div><div class="panel-body"><p>API, 行为, RTFM... 没错, 我们又再次看到了计算机系统设计的一个例子:
设备向CPU暴露设备寄存器的接口, 把设备内部的复杂行为(甚至一些模拟电路的特性)进行抽象,
CPU只需要使用这一接口访问设备, 就可以实现期望的功能.</p><p>计算机系统处处蕴含抽象的思想, 只要理解其中的原理,
再加上RTFM的技能, 你就能掌握计算机系统的全部!</p></div></div>
<h3 id="內存映射io">內存映射I/O</h3>
<p>端口映射I/O把端口号作为I/O指令的一部分, 这种方法很简单, 但同时也是它最大的缺点.
指令集为了兼容已经开发的程序, 是只能添加但不能修改的.
这意味着, 端口映射I/O所能访问的I/O地址空间的大小, 在设计I/O指令的那一刻就已经决定下来了.
所谓I/O地址空间, 其实就是所有能访问的设备的地址的集合.
随着设备越来越多, 功能也越来越复杂, I/O地址空间有限的端口映射I/O已经逐渐不能满足需求了.
有的设备需要让CPU访问一段较大的连续存储空间,
如VGA的显存, 24色加上Alpha通道的1024x768分辨率的显存就需要3MB的编址范围.
于是内存映射I/O(memory-mapped I/O, MMIO)应运而生.</p>
<p>内存映射I/O这种编址方式非常巧妙, 它是通过不同的物理内存地址给设备编址的.
这种编址方式将一部分物理内存的访问"重定向"到I/O地址空间中,
CPU尝试访问这部分物理内存的时候, 实际上最终是访问了相应的I/O设备, CPU却浑然不知.
这样以后, CPU就可以通过普通的访存指令来访问设备.
这也是内存映射I/O得天独厚的好处:
物理内存的地址空间和CPU的位宽都会不断增长, 内存映射I/O从来不需要担心I/O地址空间耗尽的问题.
从原理上来说, 内存映射I/O唯一的缺点就是,
CPU无法通过正常渠道直接访问那些被映射到I/O地址空间的物理内存了.
但随着计算机的发展, 内存映射I/O的唯一缺点已经越来越不明显了:
现代计算机都已经是64位计算机, 物理地址线都有48根, 这意味着物理地址空间有256TB这么大,
从里面划出3MB的地址空间给显存, 根本就是不痛不痒.
正因为如此, 内存映射I/O成为了现代计算机主流的I/O编址方式:
RISC架构只提供内存映射I/O的编址方式,
而PCI-e, 网卡, x86的APIC等主流设备, 都支持通过内存映射I/O来访问.</p>
<p>作为RISC架构, mips32和riscv32都是采用内存映射I/O的编址方式.
对x86来说, 内存映射I/O的一个例子是NEMU中的物理地址区间<code>[0xa1000000, 0xa1800000)</code>.
这段物理地址区间被映射到VGA内部的显存, 读写这段物理地址区间就相当于对读写VGA显存的数据.
例如</p>
<pre><code class="lang-c"><span class="hljs-built_in">memset</span>((<span class="hljs-keyword">void</span> *)<span class="hljs-number">0xa1000000</span>, <span class="hljs-number">0</span>, SCR_SIZE);
</code></pre>
<p>会将显存中一个屏幕大小的数据清零, 即往整个屏幕写入黑色像素, 作用相当于清屏.
可以看到, 内存映射I/O的编程模型和普通的编程完全一样:
程序员可以直接把I/O设备当做内存来访问. 这一特性也是深受驱动开发者的喜爱.</p>
<div class="panel panel-info"><div class="panel-heading"><h5 class="panel-title" id="理解volatile关键字"><i class="fa fa-question-circle"></i> 理解volatile关键字</h5></div><div class="panel-body"><p>也许你从来都没听说过C语言中有<code>volatile</code>这个关键字, 但它从C语言诞生开始就一直存在.
<code>volatile</code>关键字的作用十分特别, 它的作用是避免编译器对相应代码进行优化.
你应该动手体会一下<code>volatile</code>的作用, 在GNU/Linux下编写以下代码:</p><pre><code class="lang-c"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">fun</span><span class="hljs-params">()</span> </span>{
<span class="hljs-keyword">extern</span> <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">char</span> _end; <span class="hljs-comment">// _end是什么?</span>
<span class="hljs-keyword">volatile</span> <span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">char</span> *p = &_end;
*p = <span class="hljs-number">0</span>;
<span class="hljs-keyword">while</span>(*p != <span class="hljs-number">0xff</span>);
*p = <span class="hljs-number">0x33</span>;
*p = <span class="hljs-number">0x34</span>;
*p = <span class="hljs-number">0x86</span>;
}
</code></pre><p>然后使用<code>-O2</code>编译代码.
尝试去掉代码中的<code>volatile</code>关键字, 重新使用<code>-O2</code>编译, 并对比去掉<code>volatile</code>前后反汇编结果的不同.</p><p>你或许会感到疑惑, 代码优化不是一件好事情吗? 为什么会有<code>volatile</code>这种奇葩的存在?
思考一下, 如果代码中<code>p</code>指向的地址最终被映射到一个设备寄存器, 去掉<code>volatile</code>可能会带来什么问题?</p></div></div>
<h3 id="状态机视角下的输入输出">状态机视角下的输入输出</h3>
<p>我们在PA1中提到, 计算机和程序都可以看做一个状态机,
这个状态机的状态可以表示成<code>S = <R, M></code>, 其中<code>R</code>是寄存器的状态, <code>M</code>是内存的状态.
计算机添加输入输出的功能之后, 我们应该如何理解输入输出的行为呢?</p>
<p>我们可以把设备分成两部分, 一部分是数字电路.
我们刚才粗略地介绍了一些设备控制器的功能, 例如我们CPU可以从键盘控制器中读出按键信息.
既然是数字电路, 我们就可以把其中的时序逻辑电路看成是设备数字电路部分的状态<code>D</code>.
但<code>D</code>比较特殊, 计算机只能通过端口I/O指令或者内存映射I/O的访存指令来访问和修改<code>D</code>.</p>
<p>有意思的是设备的另一部分: 模拟电路, 它也可以改变<code>D</code>.
例如键盘通过检查按键位置的电容变化来判断是否有按键被按下,
若有, 则会将按键信息写入到键盘控制器的寄存器中.
而按键位置的电容是否发生变化, 又是由物理世界中的用户是否按下按键决定的.
所以我们会说, 设备是连接计算机和物理世界的桥梁.</p>
<pre><code> 状态机模型 | 状态机模型之外
S = <R, M> | D
计算机/程序 <----I/O指令----> 设备 <----模拟电路----> 物理世界
|
|
</code></pre><p>要对设备的状态和行为进行建模是一件很困难的事情,
除了设备本身的行为五花八门之外, 设备的状态还时时刻刻受到物理世界的影响.
于是, 我们在对状态机模型的行为进行扩展的时候, 并不考虑将<code>D</code>加入到<code>S</code>中,
而是仅仅对输入输出相关指令的行为进行建模:</p>
<ul>
<li>执行普通指令时, 状态机按照TRM的模型进行状态转移</li>
<li>执行设备输出相关的指令(如x86的<code>out</code>指令或者RISC架构的MMIO写指令)时,
状态机除了更新PC之外, 其它状态均保持不变, 但设备的状态和物理世界则会发生相应的变化</li>
<li>执行设备输入相关的指令(如x86的<code>in</code>指令或者RISC架构的MMIO读指令)时,
状态机的转移将会"分叉": 状态机不再像TRM那样有唯一的新状态了,
状态机具体会转移到哪一个新状态, 将取决于执行这条指令时设备的状态</li>
</ul>
<p><img src="images/ioe.png" alt="ioe"></p>
<p>例如, 上图中的程序将要执行指令<code>in addr, r</code>,
这条指令将会从设备地址<code>addr</code>中读入一个数据到CPU的寄存器<code>r</code>中.
不妨假设这个设备地址对应的是某键盘控制器,
执行这条指令之后, <code>r</code>中的值可能是<code>0x01</code>, 表示读到"按下扫描码为1的按键"的信息;
也可能是<code>0x81</code>, 表示读到"释放扫描码为1的按键"的信息;
也可能是<code>0x0</code>, 表示没有任何按键信息.
这一步不确定的状态转移会影响到后续程序的运行,
例如某个游戏会根据读入的按键信息决定如何响应,
但不难理解, 游戏如何响应都是通过TRM中普通的计算指令来实现的, 和输入输出没有关系.</p>
<p>这个扩展之后的状态机模型从微观的角度告诉我们, 设备的输入输出都是通过CPU的寄存器来进行数据交互的.
输入输出对程序的影响也仅仅体现在输入时会进行一次不能提前确定的状态转移,
这基本上就是程序眼中输入输出的全部.</p>
<div class="panel panel-info"><div class="panel-heading"><h5 class="panel-title" id="通过内存进行数据交互的输入输出"><i class="fa fa-comment-o"></i> 通过内存进行数据交互的输入输出</h5></div><div class="panel-body"><p>我们知道<code>S = <R, M></code>, 上文介绍的端口I/O和内存映射I/O都是通过寄存器<code>R</code>来进行数据交互的.
很自然地, 我们可以考虑, 有没有通过内存<code>M</code>来进行数据交互的输入输出方式呢?</p><p>其实是有的, 这种方式叫<a href="http://en.wikipedia.org/wiki/Direct_memory_access" target="_blank">DMA</a>.
为了提高性能, 一些复杂的设备一般都会带有DMA的功能.
不过在NEMU中的设备都比较简单, 关于DMA的细节我们就不展开介绍了.</p></div></div>
<h2 id="nemu中的输入输出">NEMU中的输入输出</h2>
<p>NEMU的框架代码已经在<code>nemu/src/device/</code>目录下提供了设备相关的代码,</p>
<h3 id="映射和io方式">映射和I/O方式</h3>
<p>NEMU实现了端口映射I/O和内存映射I/O两种I/O编址方式.
但无论是端口映射I/O还是内存映射I/O, 它们的核心都是映射.
自然地, 我们可以通过对映射的管理来将这两者统一起来.</p>
<p>具体地, 框架代码为映射定义了一个结构体类型<code>IOMap</code>(在<code>nemu/include/device/map.h</code>中定义),
包括名字, 映射的起始地址和结束地址, 映射的目标空间, 以及一个回调函数.
然后在<code>nemu/src/device/io/map.c</code>实现了映射的管理,
包括I/O空间的分配及其映射, 还有映射的访问接口.</p>
<p>其中<code>map_read()</code>和<code>map_write()</code>用于将地址<code>addr</code>映射到<code>map</code>所指示的目标空间, 并进行访问.
访问时, 可能会触发相应的回调函数, 对设备和目标空间的状态进行更新.
由于NEMU是单线程程序, 因此只能串行模拟整个计算机系统的工作,
每次进行I/O读写的时候, 才会调用设备提供的回调函数(callback).
基于这两个API, 我们就可以很容易实现端口映射I/O和内存映射I/O的模拟了.</p>
<p><code>nemu/src/device/io/port-io.c</code>是对端口映射I/O的模拟.
<code>add_pio_map()</code>函数用于为设备的初始化注册一个端口映射I/O的映射关系.
<code>pio_read()</code>和<code>pio_write()</code>是面向CPU的端口I/O读写接口,
它们最终会调用<code>map_read()</code>和<code>map_write()</code>, 对通过<code>add_pio_map()</code>注册的I/O空间进行访问.</p>
<p>内存映射I/O的模拟是类似的, <code>paddr_read()</code>和<code>paddr_write()</code>会判断地址<code>addr</code>落在物理内存空间还是设备空间,
若落在物理内存空间, 就会通过<code>pmem_read()</code>和<code>pmem_write()</code>来访问真正的物理内存;
否则就通过<code>map_read()</code>和<code>map_write()</code>来访问相应的设备.
从这个角度来看, 内存和外设在CPU来看并没有什么不同, 只不过都是一个字节编址的对象而已.</p>
<h3 id="设备">设备</h3>
<p>NEMU实现了串口, 时钟, 键盘, VGA, 声卡, 磁盘, SD卡七种设备,
其中磁盘会在PA的最后再进行介绍, 而SD卡在PA中不会涉及.
为了简化实现, 这些设备都是不可编程的, 而且只实现了在NEMU中用到的功能.
为了开启设备模拟的功能, 你需要在menuconfig选中相关选项:</p>
<pre><code>[*] Devices --->
</code></pre><p>重新编译后, 你会看到运行NEMU时会弹出一个新窗口, 用于显示VGA的输出(见下文).
需要注意的是, 终端显示的提示符<code>(nemu)</code>仍然在等待用户输入, 此时窗口并未显示任何内容.</p>
<p>NEMU使用SDL库来实现设备的模拟, <code>nemu/src/device/device.c</code>含有和SDL库相关的代码.
<code>init_device()</code>函数主要进行以下工作:</p>
<ul>
<li>调用<code>init_map()</code>进行初始化.</li>
<li>对上述设备进行初始化, 其中在初始化VGA时还会进行一些和SDL相关的初始化工作,
包括创建窗口, 设置显示模式等;</li>
<li>然后会进行定时器(alarm)相关的初始化工作.
定时器的功能在PA4最后才会用到, 目前可以忽略它.</li>
</ul>
<p>另一方面, <code>cpu_exec()</code>在执行每条指令之后就会调用<code>device_update()</code>函数,
这个函数首先会检查距离上次设备更新是否已经超过一定时间, 若是, 则会尝试刷新屏幕,
并进一步检查是否有按键按下/释放, 以及是否点击了窗口的<code>X</code>按钮;
否则则直接返回, 避免检查过于频繁, 因为上述事件发生的频率是很低的.</p>
<h2 id="将输入输出抽象成ioe">将输入输出抽象成IOE</h2>
<p>设备访问的具体实现是架构相关的, 比如NEMU的VGA显存位于物理地址区间<code>[0xa1000000, 0xa1080000)</code>,
但对<code>native</code>的程序来说, 这是一个不可访问的非法区间,
因此<code>native</code>程序需要通过别的方式来实现类似的功能.
自然地, 设备访问这一架构相关的功能, 应该归入AM中.
与TRM不同, 设备访问是为计算机提供输入输出的功能,
因此我们把它们划入一类新的API, 名字叫IOE(I/O Extension).</p>
<p>要如何对不同架构的设备访问抽象成统一的API呢?
回想一下在程序看来, 访问设备其实想做什么:
访问设备 = 读出数据 + 写入数据 + 控制状态.
进一步的, 控制状态本质上也是读/写设备寄存器的操作,
所以访问设备 = 读/写操作.</p>
<p>对, 就是这么简单! 所以IOE提供三个API:</p>
<pre><code class="lang-c"><span class="hljs-function"><span class="hljs-keyword">bool</span> <span class="hljs-title">ioe_init</span><span class="hljs-params">()</span></span>;
<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">ioe_read</span><span class="hljs-params">(<span class="hljs-keyword">int</span> reg, <span class="hljs-keyword">void</span> *buf)</span></span>;
<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">ioe_write</span><span class="hljs-params">(<span class="hljs-keyword">int</span> reg, <span class="hljs-keyword">void</span> *buf)</span></span>;
</code></pre>
<p>第一个API用于进行IOE相关的初始化操作.
后两个API分别用于从编号为<code>reg</code>的寄存器中读出内容到缓冲区<code>buf</code>中,
以及往编号为<code>reg</code>寄存器中写入缓冲区<code>buf</code>中的内容.
需要注意的是, 这里的<code>reg</code>寄存器并不是上文讨论的设备寄存器, 因为设备寄存器的编号是架构相关的.
在IOE中, 我们希望采用一种架构无关的"抽象寄存器",
这个<code>reg</code>其实是一个功能编号, 我们约定在不同的架构中,
同一个功能编号的含义也是相同的, 这样就实现了设备寄存器的抽象.</p>
<p><code>abstract-machine/am/include/amdev.h</code>中定义了常见设备的"抽象寄存器"编号和相应的结构.
这些定义是架构无关的, 每个架构在实现各自的IOE API时, 都需要遵循这些定义(约定).
为了方便地对这些抽象寄存器进行访问, klib中提供了<code>io_read()</code>和<code>io_write()</code>这两个宏,
它们分别对<code>ioe_read()</code>和<code>ioe_write()</code>这两个API进行了进一步的封装.</p>
<p>特别地, NEMU作为一个平台, 设备的行为是与ISA无关的,
因此我们只需要在<code>abstract-machine/am/src/platform/nemu/ioe/</code>目录下实现一份IOE, 来供NEMU平台的架构共享.
其中, <code>abstract-machine/am/src/platform/nemu/ioe/ioe.c</code>中实现了上述的三个IOE API,
<code>ioe_read()</code>和<code>ioe_write()</code>都是通过抽象寄存器的编号索引到一个处理函数, 然后调用它.
处理函数的具体功能和寄存器编号相关, 下面我们来逐一介绍NEMU中每个设备的功能.</p>
<h3 id="串口">串口</h3>
<p>串口是最简单的输出设备. <code>nemu/src/device/serial.c</code>模拟了串口的功能.
其大部分功能也被简化, 只保留了数据寄存器.
串口初始化时会分别注册<code>0x3F8</code>处长度为8个字节的端口,