-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathdemand_skeleton_analysis.txt
2641 lines (2322 loc) · 115 KB
/
demand_skeleton_analysis.txt
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
将需求按模块划分:
1.bootloader
思路一:
loader1:
放置在boot setctor中;
加载loader2并将控制转移到loader2;
需要理解FAT12文件系统
先建立一个虚拟软盘(格式化为FAT12文件系统),向文件系统中添加文件,通过od查看文件系统结构变化
再构造一个boot sector,在该sector中加入BPB信息,将该sector写入一个软盘,该软盘即被格式化为FAT12文件系统
随后可以将loader2写入该文件系统中,loader1通过FAT12文件系统读取
需要一个根据文件名(仅限于根目录中)在FAT12中查找,并读取其全部内容到指定缓存区的过程
首先,通过读取扇区过程读取根目录所在的扇区
按照文件名查找文件,未找到则提示出错
根据文件第一个cluster号,通过读取扇区过程,读取一个cluster到指定缓存区
再查看FAT表(一个过程),是否为最后一个,不是则继续读取剩余cluster
需要一个打印函数
loader2:
放置在FAT12文件系统中;
协定loader1转移到loader2的过程(段基地址、段内偏移)
获取可用内存信息
设置GDT
lgdtr
打开A20
打开CR0的PE位
刷新CS/DS/SS/ES/FS/GS段描述符高速缓存器
加载kernel并将控制转移到kernel
理解ELF格式
学习C语言和汇编语言混合编程
思路二:
使用grub做loader
理解ld如何链接程序
了解grub将内核加载进内存后,内存中的状态
2.中断、系统调用管理机制
构想:
在IDT中设置中断门和陷阱门,并且中断门的DPL均为0,陷阱门DPL为3,中断门供异常和外部硬件中断使用,陷阱门用于系统调用,因此在系统调用中是可以被中断的。
关于中断处理的架构,参考interrupt_process_explain.png;
用户程序应该通过int 255发起系统调用,并在eax中指定系统调用号(指定进行哪个系统调用);参数传递方式则参照cdecl;返回值保存在eax中,和cdecl保持一致;
用户进程通过系统调用进入内核后,内核将根据系统调用号在系统调用表中找到对应的处理函数并转而执行该函数;其返回的结果保存在该进程的中断帧中的eax中;然后按照通用的中断返回机制返回用户模式;按照cdecl,调用者可像调用普通函数一样获得系统调用的返回值;
系统调用表为一个函数指针列表,指向具体执行系统调用工作的函数,所使用的函数原型是:
int32_t func(void)
其返回值用作该系统调用的结果;
总体思路是:
定义IDT的相关结构
定义各个中断处理程序
设置好IDT
配置8259A PIC,初始时禁止任何中断进入
定义中断的分发处理
注意:
在内核中不能使用int n的形式发起那些本身就有错误代码的中断,这样会破坏栈平衡;
不要发起以20-31这些中断向量相关的中断,因为这些未定义(20也包括在内,因为测试的bochs上可能没有启用该CPU特性);
在测试中,有出现过未处理的中断0x27(但实际该中断应该已经在IMR中屏蔽),怀疑是suprious IRQ;
文件的组织结构:
idt.h 供init使用,应该提供:定义IDT条目结构、定义IDTR结构、
定义用于保存中断发生时的现场信息的结构、初始化IDT和注册处理特定中断的函数原型
idt.c 实现初始化IDT、注册处理特定中断的函数
idt.asm 实现中断处理程序中通用的保护现场过程和恢复现场过程、刷新idtr的过程
8259A.c 实现初始化两个8259A PIC、设置8259A的IMR
8259A.h 提供初始化8259A PIC、设置IMR的函数原型、IRQ与中断向量的映射
syscall.c 提供支撑系统调用的框架函数实现
syscall.h 提供上述函数的接口声明
提供的功能:
初始化IDT:
建立IDT表,刷新IDTR寄存器,按照需要注册处理特定中断的函数
注册处理特定中断的函数:
将特定函数记录在中断处理函数表中
初始化8259A PIC
发送四个ICW
ICW1: 0001 0001 (0x11),通过命令端口写入
ICW2:
MASTER: 0x20(master 8259A PIC被映射到0x20-0x27这8个中断向量),通过数据端口写入
SLAVE: 0x28(slave 8259A PIC被映射到0x28-0x2F这8个中断向量),通过数据端口写入
ICW3:
MASTER: 0X04(MASTER上的IRQ2被用作链接SLAVE),通过数据端口写入
SLAVE: 0x02(SLAVE的INT引脚链接到MASTER的IRQ2上),通过数据端口写入
ICW4: 0x01,通过数据端口写入
设置主从片的IMR,禁止任何中断进入
发送EOI给PIC
向master PIC发送EOI
如果中断向量大于等于0x28
同时需要向slave PIC发送EOI
各个中断入口:
根据具体中断的不同,压入中断向量号,如果CPU没有压入错误码,则还需压入错误码;
通用的保存现场:
保存中断现场到中断帧中;
通用的恢复现场,从中断中返回:
恢复中断帧中的信息;
回到原来的状态继续执行;
中断分发:
根据中断向量,分类处理
内部中断或异常(0~31)
如果有相关处理函数
调用相关函数
否则输出中断信息,并PANIC
外部硬件中断(通过8259A PIC发起的中断,目前设置为32~47):
键盘、时钟、IDE硬盘中断处理:
发送EOI
调用相关中断处理函数
COM接口、软盘等硬件中断:
目前暂未提供相关支持,直接输出中断信息
通过int 255发起的中断(系统调用):
使当前进程的中断帧指针指向当前中断帧的位置;
调用函数,将控制分发到指定的系统调用函数上;
其他中断:
输出中断信息,并PANIC
根据当前进程指定的系统调用号,执行对应的系统调用函数:
如果系统调用号在允许范围内且有效
调用对应的函数;
保存返回结果到当前进程的中断帧中的eax中;
否则PANIC;
检查当前进程用户地址空间地址addr是否在非用户栈的有效地址空间中,即是否可以通过该地址访问至少一个字节的数据:
addr 用户地址空间中的地址
成功返回1,失败返回0
如果addr>=PROC_LOAD_ADDR且addr<PROC_LOAD_ADDR+当前进程对应PCB.size
返回1;
否则返回0;
检查当前进程用户地址空间地址addr是否在用户栈中,即是否可以通过该地址访问至少一个字节的数据:
addr 用户地址空间中的地址
成功返回1,失败返回0
如果addr>=用户栈起始位置且addr<用户栈结束位置
返回1;
否则返回0;
从当前进程用户地址空间addr处取出一个4字节大小的参数到ap指定位置处:
addr 用户地址空间中的地址
ap 指向保存参数的位置
成功返回0且结果将写入*ap中,失败返回-1且*ap中的值不会被修改;
调用函数,检查addr和addr+3是否都在用户栈中或者都在非用户栈的有效地址空间中
如果是
从addr处读取4字节到ap指向的位置中;
返回0;
返回-1;
检查当前进程用户地址空间addr处开始是否为一个NUL结尾的字符串:
addr 用户地址空间中的地址
是则返回其长度,否则返回-1;
调用函数,检查addr是否属于非栈空间的有效用户地址空间地址
如果是
如果addr所指示字符为NUL
停止检查,返回此时已经检查过的字符个数;
令addr指向下一个字符;
检查addr是否属于非栈空间的有效用户地址空间地址
如果是
则继续上述循环;
返回-1;
调用函数,检查addr是否属于栈空间
如果是
如果addr所指示字符为NUL
停止检查,返回此时已经检查过的字符个数;
令addr指向下一个字符;
检查addr是否属于栈空间
如果是
则继续上述循环;
返回-1;
返回-1;
从当前进程用户栈中取出第n个4字节大小的参数写入到ap所指向的位置,遵循cdecl:
n 从左往右第n个4字节大小的参数,n从0开始
ap 指向保存参数的位置
成功返回0且参数将写入ap所指向的位置,失败返回-1且*ap中的值不会被修改;
根据进程用户栈当前esp的位置,找到第n个参数的地址m;
调用函数,从m处取出4字节大小的整数,写入ap所指向的位置,并返回该函数调用的结果;
从当前进程用户栈取出第n个4字节大小的参数作为指针,写入到pp所指向的位置,并检查该指针指向的对象是否在进程的有效用户地址空间中,遵循cdecl:
n 从左往右第n个4字节大小的参数,n从0开始
pp 指向保存指针的位置
size 该指针指向的对象大小
成功返回0且参数将写入pp所指向的位置,失败返回-1且*pp中的值不会被修改;
调用函数,从当前进程用户栈中取出第n个4字节大小的参数,保存在tmp中,返回结果保存在ret中;
如果ret等于-1
返回-1;
如果tmp+size<tmp
说明溢出了,返回-1;
如果tmp和tmp+size都在用户栈中或者都在非用户栈的有效地址空间中
将tmp写入pp所指向的位置;
返回0;
返回-1;
从当前进程用户栈中取出第n个4字节大小的参数作为字符串指针,写入pp所指向的位置,并检查该指针指向的对象是否为NUL结尾的字符串,遵循cdecl:
n 从左往右第n个4字节大小的参数,n从0开始
pp 指向保存指针的位置
如果参数取出成功且所指向对象确实是NUL结尾的字符串,则返回字符串长度且字符串指针将写入pp所指向的位置,否则返回-1且*pp中的值不会被修改;
调用函数,从当前进程用户栈中取出第n个4字节大小的参数,保存在tmp中,返回结果保存在ret中;
如果ret等于-1
返回-1;
调用函数,检查tmp处是否为一个NUL结尾的字符串,返回结果保存在ret2中;
如果ret2为-1
返回-1;
将tmp写入pp所指向的位置;
返回ret2;
3.内存管理
《1》基础的内存管理:
构想:
每个进程的地址空间分为用户地址空间和内核地址空间,调度进程则只有内核地址空间;不管什么进程,其内核地址空间都是相同的,在创建进程时,所有可用物理内存均被映射到内核地址空间中(根据grub提供的mmap信息),之后一般不再修改;用户地址空间所需物理内存同样从内核地址空间分配,然后再映射到用户地址空间,也就是说对于用户地址空间映射的物理内存,在内核地址空间中同样存在映射;这种设计,能够保证对于每个进程,其内核空间地址都是统一的,但其用户空间地址则是独立的,而且一个进程可以通过内核空间地址访问另一个进程的用户空间数据,这有利于fork/exec/wait/exit这类调用的实现。
设定KERNEL_VD_OFFSET为0xC0000000,内核地址空间从这里开始;设定kernel_start_addr/kernel_end_addr分别为内核被加载到内存之后的起始虚拟地址和结束虚拟地址。目前暂定只支持64MB(0x4000000,SUPPORT_MEM_SIZE)的内存,使用4KB大小的页面。
具体化内存布局如下:
0x0 ~ KERNEL_VD_OFFSET,映射到内核所分配的物理内存,用作用户地址空间(代码、数据、用户栈、堆);
KERNEL_VD_OFFSET ~ KERNEL_VD_OFFSET+0x100000,映射到0x0 ~ 0x100000,暂不使用;
KERNEL_VD_OFFSET+0x100000 ~ PAGE_UPPER_ALIGN(kernel_end_addr),映射到0x100000 ~ PAGE_UPPER_ALIGN(K_V2P(kernel_end_addr)),用于内核代码、数据、初始内核栈、初始内核分页结构;
PAGE_UPPER_ALIGN(kernel_end_addr) ~ KERNEL_VD_OFFSET+SUPPORT_MEM_SIZE,映射到PAGE_UPPER_ALIGN(K_V2P(kernel_end_addr)) ~ SUPPORT_MEM_SIZE,剩余空闲物理内存;
这里假定从物理地址0x100000开始为内核管理的内存,通过grub来确定其结束位置;每个新建进程都拥有这样的内存布局。
将进入分页模式的工作分为两部分(boot1/boot2):
使boot1的LMA和VMA相同,boot2的LMA仍然紧接boot1的LMA,但VMA设置为从KERNEL_VD_OFFSET开始。boot1负责接受从GRUB传来的控制,建立页目录和两类页表:
第一类页表负责将虚拟地址0x0 ~ SUPPORT_MEM_SIZE映射到物理地址0x0 ~ SUPPORT_MEM_SIZE,便于boot1开启分页模式之后,仍能继续执行;
第二类页表负责将虚拟地址KERNEL_VD_OFFSET ~ KERNEL_VD_OFFSET+SUPPORT_MEM_SIZE映射到物理地址0x0 ~ SUPPORT_MEM_SIZE,这类页表就是内核之后的运行地址空间;
然后开启分页模式,转入到boot2部分。由于提前建立上述两类页表,所以在开启分页模式之后,进入boot2部分之前,仍然能正常执行指令。在进入boot2部分之后,撤销掉第1类页表,使得0-3GB的线性地址空间能被用户程序使用,并根据mmap信息初始化基础的内存管机制,再继续做其余的初始化工作。
在建立这两类页表时,事实上只需要建立一类页表,让两类不同的页目录项指向这类页表即可;同时,初始时页目录条目属性设置为用户特权级别的,而页表条目则设置为内核特权级别的,对于其他新建分页结构也是如此,一般情况使用页表条目进行特权级控制。
在开启分页之后,转入boot2之前,应该修改Multiboot information结构、栈(esp/ebp)的地址、显存地址,以保证撤销掉第1类页表之后还能正确访问这些地址;这些数据的物理地址仍然不改变,也就是说不会再使用新的内存区域,仅仅修改其线性地址。
根据bochs上的实验,配置64MB的内存并不代表所有的物理内存均可访问,将根据mmap信息对分页结构进行调整(将某些页表项置0)。
根据Multiboot 0.6.96,不能保证启动信息保存的位置,但是当前仍然假设其信息均保存在0x100000以下的位置;对于0x0 ~ SUPPORT_MEM_SIZE之间的内存全部都映射,而不管其中可能存在的非available区域;设定内核中可用于动态分配的内存范围从PAGE_UPPER_ALIGN(kernel_end_addr)到从该地址往上的第一个不可用(非available)内存页位置。
将可用于动态分配的内存通过单向链表链接起来,链表头指向第一个空闲页面,该空闲页面的开头位置存放指向下一个空闲页面的指针,如此下去,最后一个空闲页面中的指针置为NULL;每次分配/释放则是对空闲页面链表进行抽取/插入操作,每次只能分配或释放一个页面;为分配和释放操作提供两类接口,一类接口在操作时需要请求锁,另一类则不需要,在已经关闭中断的情况下可以使用无需锁的接口,而在打开中断的时候使用需要请求锁的接口;
链表头结构定义如下:
flags,表明当前是否已经上锁(BUSY);
head,空闲页指针,指向链表中第一个空闲页面;
end_addr,可用于动态分配的内存结束位置;
系统只需要定义一个链表头结构变量。
需要的接口:
启动信息结构操作接口:
检查指定的物理页框是否为有效物理内存,成功返回1,失败返回0(物理地址paddr):
遍历mmap条目,如果该页框在有效(available)物理内存中,则返回1,否则返回0;
显示该机器的物理内存布局信息:
刷新整个TLB中除具有全局属性的其他条目:
刷新CR3/读取CR3
刷新CR3时,会更换页目录,更改PCD/PWT属性,同时刷新整个TLB(除具有全局属性的条目之外的所有条目);
处理page fault
目前暂时只输出导致page fault的原因,不作进一步处理;
在虚拟地址(线性地址)和物理地址之间转换,虚拟地址仅限于KERNEL_VD_OFFSET ~ KERNEL_VD_OFFSET + SUPPORT_MEM_SIZE,物理地址仅限于0x0 ~ SUPPORT_MEM_SIZE
p2v/v2p
初始化页分配机制:
从PAGE_UPPER_ALIGN(kernel_end_addr)开始,直到K_P2V(SUPPORT_MEM_SIZE),记录所遇到的第一个不可用(非available)内存页的地址addr;
将addr写入链表头结构的end_addr域中;
设置链表头结构中的flags为UN-BUSY;
将所有可用空闲页链接起来,第一个空闲页位置写入链表头结构中的head域;
无需锁,分配一个清零过的空闲页,返回该页面起始虚拟地址:
如果已经没有空闲页可分配,则返回NULL;
从链表头中拿出一个空闲页面pg,并使链表头中的指针指向链表中下一个空闲页;
清零所分配的空闲页;
返回所分配的空闲页起始地址;
无需锁,回收一个PAGE_DOWN_ALIGN(vaddr)所指定的页面(页面地址vaddr):
检查vaddr指定的页面是否在PAGE_UPPER_ALIGN(kernel_end_addr) ~ KERNEL_VD_OFFSET+SUPPORT_MEM_SIZE范围里有效的内存中;
如果有一项不成立则PANIC;
使用1来填充该页面,期望能检查出一些错误;
将该页面插入链表中;
锁住链表头:
关闭中断;
如果flags为BUSY的
睡眠在链表头结构变量上;
返回继续检查flags变量;
设置flags为BUSY的;
恢复原先的中断状态;
解锁链表头:
如果flags不是BUSY的
可能有错误发生,PANIC;
清除flags中的BUSY标志;
唤醒在链表头结构变量上睡眠的进程;
需要锁,分配一个清零过的空闲页,返回该页的起始虚拟地址:
调用函数,锁住链表头;
调用函数,分配一个清零过的空闲页,返回其结果;
调用函数,解锁链表头;
需要锁,回收一个空闲页(空闲页地址vaddr):
调用函数,锁住链表头;
调用函数,回收vaddr指示的空闲页;
调用函数,解锁链表头;
文件的组织结构:
multiboot.c: 提供用于处理与Multiboot相关数据结构的函数;
multiboot.h: 提供Multiboot相关数据结构定义,同时包含指向grub提供的Multiboot information
struct的指针的拓展声明(在init.c中给出该定义);
vmm.c: 基础vm操作函数;
vmm.h:提供相关函数原型、页大小等常量和相关宏;
page_fault.c: page fault的处理;
《2》高层的内存管理工具:
构想:
该层在上一层的基础上,为进程创建、操纵进程内存等提供相关工具函数接口。
需要的接口:
获得虚拟地址在指定分页结构中所对应的页表条目虚拟地址:
create标志指定是否需要创建相关页表
pgdir指定页目录虚拟地址
addr指定虚拟地址
使用addr的页目录索引部分查找页目录,找出对应的页目录条目pde;
如果pde无效(不存在映射页表)
如果指定create
分配一页清零过的内存存放中间页表;
在pde中写入映射关系,pde设置为用户特权级的;
否则
返回NULL;
从pde中取出中间页表的物理地址,调用函数转化为虚拟地址;
使用addr的页表索引部分查找中间页表,找出对应的页表条目pte;
返回pte地址;
在指定分页结构中,将一段虚拟地址空间映射到一段物理地址空间:
pgdir指定页目录虚拟地址
vaddr指定起始虚拟地址,必须按页对齐
size指定空间大小
paddr指定起始物理地址,必须按页对齐
pte_attr指定页表表项的属性
让size按页对齐,不足一页的部分按一页计算;
从vaddr到vaddr+size地址区间中,调用函数,获得每一页对应的pte地址,必要时创建中间页表,其对应的pde设置为用户特权级别的;
如果该位置已经存在映射
PANIC;
使用对应物理地址和pte_attr设置pte;
创建进程的初始内核地址空间,并返回新建页目录的虚拟地址:
分配一页清零的内存存放新页目录pgdir;
调用函数,将物理地址区域0x0 ~ SUPPORT_MEM_SIZE映射到pgdir中的KERNEL_VD_OFFSET ~ KERNEL_VD_OFFSET + SUPPORT_MEM_SIZE处,对应PTE为系统特权级别,PDE为用户特权级别
成功返回新建页目录的虚拟地址,失败PANIC;
为pgdir的用户地址空间中start_addr到end_addr这段地址空间分配内存,成功返回end_addr,失败返回NULL且被分配的内存将被释放掉:
pgdir 指定分页结构
start_addr 起始地址,可以不按页对齐,分配时将从PAGE_UPPER_ALIGN(start_addr)开始
end_addr 结束地址,可以不按页对齐,分配时将截至至PAGE_UPPER_ALIGN(end_addr)
如果pgdir为NULL,则PANIC;
如果end_addr大于KERNEL_VD_OFFSET
返回NULL;
如果end_addr小于等于start_addr
返回start_addr
令addr为PAGE_UPPER_ALIGN(start_addr);
如果addr < end_addr,则循环执行下述语句
调用函数,分配一页清零的内存,结果保存在page中;
如果page为NULL
调用函数,释放pgdir中从start_addr到end_addr之间映射的内存(如果存在的话);
返回NULL;
调用函数,将K_V2P(page)这页内存映射到pgdir中从addr开始的位置,PTE属性设置为用户特权级、可读可写;
令addr向上偏移一个页面大小;
返回end_addr;
注意:如果end_addr小于等于start_addr,则不会分配内存,并且返回start_addr
释放指定分页结构的用户地址空间中从start_addr到end_addr之间可能被映射的内存,成功返回start_addr,发生错误则PANIC:
pgdir 指定分页结构
start_addr 起始地址,可以不按页对齐,释放时从PAGE_UPPER_ALIGN(start_addr)开始
end_addr 结束地址,可以不按页对齐,释放时将截至至PAGE_UPPER_ALIGN(end_addr)
如果pgdir为NULL,则PANIC;
如果end_addr大于KERNEL_VD_OFFSET
PANIC;
如果end_addr小于等于start_addr
返回start_addr;
令addr为PAGE_UPPER_ALIGN(start_addr);
如果addr小于end_addr,则循环执行下述语句
调用函数,获得addr在pgdir中对应的页表项,结果保存在pte中;
如果pte为NULL
说明没有中间页表,从下一个4MB开始位置继续处理;
令addr向上偏移4MB;
令addr和0xFFC00000按位相与,结果保存在addr中;
再令addr向下偏移一个页面大小;
否则
如果pte有效
取出pte中的物理地址,结果保存在paddr中;
调用函数,释放K_P2V(paddr)这个页面;
将pte清零;
令addr向上偏移一个页面大小;
返回start_addr;
注意:如果end_addr小于等于start_addr,则不会释放内存
释放分页结构管理的内存以及整个分页结构,不包括内核地址空间所映射的内存,失败则PANIC:
pgdir 待释放的分页结构
如果pgdir为NULL,则PANIC;
调用函数,释放pgdir中从PROC_LOAD_ADDR到KERNEL_VD_OFFSET之间可能被映射的内存;
从头遍历pgdir的页目录项
如果该目录项有效
取出该目录项中的物理地址部分,结果保存在paddr中;
调用函数,释放K_P2V(paddr)所对应的页面;
调用函数,释放pgdir所占用的页面;
将指定分页结构中的用户地址空间地址转换为内核地址空间地址,成功返回映射后结果,失败返回NULL:
pgdir 指定分页结构
uvaddr 指定用户地址空间地址
如果pgdir为NULL,则PANIC;
调用函数,获得uvaddr在pgdir中对应的页表项的地址,结果保存在pte中;
如果pte为NULL或者pte无效或者pte所表示的条目没有设置用户特权
返回NULL;
取出pte中的物理地址,结果保存在paddr中;
返回K_P2V(paddr) + PG_OFFSET(uvaddr);
从指定位置pos复制len个字节到指定分页结构pgdir中的addr处,成功返回0,失败返回-1:
pgdir 指定分页结构
addr 目标地址
pos 起始位置
len 需要复制的字节数
如果pgdir为NULL,则PANIC;
当len > 0时,循环执行下述语句
调用函数,将addr转换为内核地址空间地址,结果保存在kvaddr中;
如果kvaddr为NULL
说明addr没有被映射到内存或者addr具有内核权限属性,返回-1;
待复制的字节数n则取PAGE_DOWN_ALIGN(kvaddr)+PAGE_SIZE-kvaddr和len之间的最小值;
调用函数,复制从pos开始的n个字节到kvaddr中;
令len为其减去n之后的值;
令addr/pos向上偏移n个字节位置;
返回0;
注意:
该函数假定从addr到addr+len之间是存在内存映射的;
该函数仅适用于复制数据到用户权限的地址空间中,即addr到addr+len之间的地址是具有用户权限的;
创建一个指定地址空间的副本:
pgdir: 被复制的地址空间;
size: 从PROC_LOAD_ADDR开始的有效用户地址空间大小,不包括用户栈;
成功返回所创建副本的页目录地址,失败返回NULL;
调用函数,创建初始的进程地址空间,保存在new_pgdir中;
针对从PROC_LOAD_ADDR到PAGE_UPPER_ALIGN(PROC_LOAD_ADDR+size)之间的每一页内存
调用函数,获得该页内存在pgdir中对应的页表项条目,结果保存在pte中;
如果无中间页表或者pte中没有设置P位
跳过该页内存,继续处理下一页;
调用函数,在new_pgdir中分配对应的位置的内存
如果分配失败
调用函数,释放与new_pgdir关联的所有内存;
返回NULL;
调用函数,获取上述分配内存在new_pgdir中对应的页表项条目,保存在new_pte中;
转换pte以及new_pte中的物理地址为内核地址空间地址,并调用函数,将pte指定物理页复制到new_pte指定物理页中;
调用函数,在new_pgdir中所设定用户栈的位置分配内存,用作用户栈;
如果分配失败
调用函数,释放与new_pgdir关联的所有内存;
返回NULL;
针对用户栈中的每一页内存
调用函数,获得该页内存在pgdir中对应的页表条目,结果保存在pte中;
调用函数,获取该页内存在new_pgdir中对应的页表项条目,保存在new_pte中;
转换pte以及new_pte中的物理地址为内核地址空间地址,并调用函数,将pte指定物理页复制到new_pte指定物理页中;
返回new_pgdir;
注意:
副本中用户地址空间内容是相同但独立的,内核地址空间内容是相同的但并不独立;
如果原地址空间中有hole,则复制后的地址空间中也有hole;
如果复制失败则之前所分配的内存均将被释放掉;
创建进程的初始内核地址分页结构,并返回新建页目录的虚拟地址【注意该函数使用方式】
函数执行逻辑等同于上述函数,但在建立映射关系时,使用的是接口是受限的!
注意:在使用该接口时,要能够保证没有其他进程在使用带锁的内存分配接口
获得虚拟地址在指定分页结构中所对应的页表条目虚拟地址【注意该函数使用方式】
函数执行逻辑等同于上述函数,除了在分配内存时,使用的是不带锁的分配内存接口!
注意:在使用该接口时,要能够保证没有其他进程在使用带锁的内存分配接口
在指定分页结构中,将一段虚拟地址空间映射到一段物理地址空间【注意该函数使用方式】
函数执行逻辑等同于上述函数,但在获取页表条目虚拟地址时,使用的是接口是受限的!
注意:在使用该接口时,要能够保证没有其他进程在使用带锁的内存分配接口
初始化第一个进程的用户地址空间
pd: 指向该进程的分页结构
init_proc_start_addr: init程序被加载到内存中的起始物理地址
init_proc_size: init程序被加载进内存后的大小
如果init_proc_size大于PAGE_SIZE,则PANIC;
调用函数,分配一个清零过的页面pg;
调用函数,在pd中,将pg对应的物理页框映射到PROC_LOAD_ADDR开始的位置,注意其对应PTE/PDE均为可读可写、用户特权级别;
将从K_P2V(init_proc_start_addr)开始的init_proc_size大小的内存复制到页面pg中,这样等同于复制到了pd中PROC_LOAD_ADDR开始的位置;
设置该进程的用户栈;
文件的组织结构:
vm_tools.c: 上述函数接口的实现;
vm_tools.h: 相关函数接口声明;
4.进程管理
构想:
在这个系统中,通过页表,使得每个进程都具有独立的地址空间,一部分为用户地址空间(0-3GB),另一部分为内核地址空间,参考内存管理部分。每个进程的用户空间中,从0x1000(PROC_LOAD_ADDR)开始依次是进程的指令、全局变量、大小可拓展的堆区、固定大小的栈区;栈区紧靠内核地址空间,在其下方安排一个未被映射的页,用于检测栈溢出;0x0 ~ PROC_LOAD_ADDR不映射,用于检测NULL指针;进程被创建时其有效用户地址空间包括初始的进程镜像和一个用户栈。
在用户地址空间中运行时,使用用户指令、用户堆栈,处于RING3中;在内核地址空间运行时,使用内核指令、内核或者用户堆栈,处于RING0中。
使用PCB(Process Control Block)来维护一个进程的状态,PCB中包含页表、内核栈位置、当前运行状态。具体结构参见PCB_explain。使用一张PCB表,跟踪系统中所有进程,表项数量为系统中并行进程数的最大值;状态为UNUSED的PCB可以被回收利用。
每个进程拥有一个运行线程。当进程刚刚被创建时为NEWBORN状态,具备可执行条件时为RUNNABLE状态,正在占用当前CPU时为RUNNING状态,等待某种事件发生的过程中为SLEEPING,进程执行结束后等待父进程回收其资源时为ZOMBIE状态。
每个进程都有用户栈(在用户空间中)和内核栈(在内核空间中)。当进程运行用户指令时,只有其用户栈被使用,其内核栈则是空的。然而当进程(系统调用、硬件中断、异常)进入内核时,内核代码就在进程的内核栈中执行;进程处于内核中时,其用户栈仍然保存着数据,只是暂时处于不活跃状态。进程交替地使用用户栈和内核栈。内核栈是用户代码无法使用的。
内核栈和用户栈为固定4KB大小。
进程不使用LDT,所有需要的段描述符都安装在GDT中。不是每个进程使用一个单独的TSS,而是共享共一个,TSS中不使用IO映射位图,所以在用户模式下的所有进程是无法执行I/O指令的。
进程切换只能发生在内核模式中。进入内核模式后,首先切换到CPU上的scheduler内核线程,然后再切换到某个进程的内核线程中,并从内核模式返回用户模式执行。scheduler线程具有自己的内核上下文和内核栈(为系统初始化时使用的内核栈),其地址空间只具有内核部分,没有用户部分。
进程初始创建时,其内核上下文中默认开启中断。在内核中,进程可以在需要时暂时关闭中断,执行关键代码之后,再恢复之前的中断状态,最多可以进行10次这样的嵌套操作。
一个进程可以暂时休眠,等待某个特定事件发生,当该事件发生时,另一个进程可以唤醒它;在当前设计中,进程可以在一个等待队列上休眠,每次针对该等待队列的唤醒都将使所有在这个等待队列上休眠的进程RUNNABLE;目前使用内核地址空间中一个数据结构的地址作为每一个等待队列的标识。进程休眠之后,将会把CPU交给调度线程。进程从休眠中醒来后,应该再次检查所等待的事件是否已经发生。
在目前的设计中,scheduler线程是不可调度的,所以scheduler不能睡眠;由于在scheduler中暂时的开启了外部硬件中断,所以在外部硬件中断处理程序中不能睡眠,因为这可能导致scheduler睡眠。
系统通过fork操作创建一个当前进程的副本作为子进程,执行该操作的则为其父进程;fork创建的子进程默认拥有和父进程相同的地址空间内容,用户地址空间内容是独立的,而内核地址空间则是相同的,所以子进程对于用户地址空间内容的修改对于父进程是不可见的;除此之外,父子进程的当前工作目录是相同的,拥有相同且独立的打开文件列表。执行fork操作后,将分别返回到父进程和子进程中,父子进程可以分别独立的运行。子进程拥有指向其父进程PCB的指针。
进程结束时会发起exit操作,这将释放自己所占用的部分资源,并将自己的所有子进程交给初始进程作为其子进程,如果有子进程已经进入ZOMBIE状态则唤醒初始进程对其处理,保存退出时的状态,然后进入ZOMBIE状态,唤醒并等待父进程对其进行进一步处理,ZOMBIE的进程无法再次运行。
父进程可以通过wait操作等待它的某个子进程进入ZOMBIE,如果发现ZOMBIE子进程则回收其占用的剩余资源,在wait时,如果没有子进程退出则父进程将睡眠下去,直到某一个子进程退出。
系统所创建的第一个可调度进程为uinit,后续所有进程均为该进程的子进程或者孙子进程,该进程将循环wait,回收那些已经ZOMBIE的子进程的资源,参考用户程序/进程构建及运行一节。
每个进程都具有一个工作目录,在解析文件系统路径时如果没有指定该路径是绝对路径,则从当前进程的工作目录开始解析;
与CPU相关的数据结构如下:
GDT
IDT
TSS
pgdir scheduler线程的页目录虚拟地址
scheduler的context指针
cur_proc 当前运行在CPU上的proc,不包括scheduler,为空时表明没有运行的进程
PCB_explain:
pid 进程id
name[] 数组,用于保存当前进程名字
state 进程状态
pgdir 进程页目录虚拟地址
kstack 内核栈起始位置(不是内核栈esp位置)
trapframe 指针,指向位于当前进程内核栈中的中断信息起始位置
context 指针,指向位于当前进程内核栈中的内核线程上下文信息
eflags_stack 保存进行pushcli/popcli之前的eflags值
eflags_sp 上一次压入的eflags在eflags_stack中的位置
channel 等待队列号,应该是内核地址空间中一个数据结构的地址
cwd 指向当前工作目录的inode结构指针【待更新,建议初始时设置context.eflags开启中断,以便在forkret中对其进行初始化;或者在进入forkret之后再开启中断,然后再初始化该域】
open_files[] 进程的打开文件指针列表
size 从PROC_LOAD_ADDR开始的有效用户地址空间大小,不包括用户栈
parent 父进程的PCB指针
retval 退出时的状态值
trapframe:
eip/cs/eflags/esp/ss; 进程中断时被CPU压入的信息(存在特权级转换时esp/ss为有效值)
trap_no/err_code; 中断程序和CPU共同负责压入
gs/fs/es/ds; 进程被中断时需要保存的CPU寄存器
edi/esi/ebp/_esp/ebx/edx/ecx/eax;
context:
eip/edi/esi/ebp/ebx/eflags; 进程的内核线程的上下文信息,在swtch函数中进行保存,由于是通过C函数调用该函数,所以不需要保存eax/ecx/edx,由于是在内核中切换,所以cs/ss/ds/es/fs/gs不需要保存,esp由context指针隐式的保存
整体思路是:定义出一个进程所必需的数据结构;然后手动构造出第一个进程;再切换到该进程继续做剩余的系统工作。
首先构造出一个PCB,在PCB中定义好进程的初始状态;然后将加载关键寄存器,使得当前执行的代码成为一个正在运行在内核空间的进程;通过特殊构造的内核栈,使得这个进程看起来是从用户空间进入内核的;然后通过通用的返回机制返回到用户空间中执行;
测试进程的构建:
将所有测试进程的代码和数据段都链接在内核结尾处,其LMA从之前段延续,VMA则设置为从0x0开始,使用特定链接器变量来指明各个测试进程的VMA;除了在内核空间中对该区域有映射外,还将该区域映射到从0x0开始的用户空间上,同时设置对应的PTE/PDE具有RW属性,使得能够在测试进程中对其数据进行读写;
整个内核工作的流程是:
BIOS初始化,将控制转入GRUB;
GRUB加载内核、进入保护模式,将控制转入内核;
boot1中设置初始页表,进入分页模式;
boot2中取消低端虚拟地址0MB ~ SUPPORT_MEM_SIZE的映射
设置CPU.pgdir,即scheduler线程的内核页目录
设置CPU.cur_proc为NULL
初始化GDT(包括在GDT中安装TSS描述符、用户模式的代码段和数据段描述符)
初始化IDT,尚未注册任何中断处理函数
初始化8259A,关闭除主片上与从片相连的IRline之外的所有IRline
初始化8253PIT,初始频率为100(每秒产生100次时钟中断),打开8259A中对应的IMR位
初始化vmm
初始化PCB表
调用用于构造第一个进程的函数,确保该进程RUNNABLE
调用调度器函数,该进程是第一个RUNNABLE的进程,将被调度执行
进行时钟中断处理测试
进行多进程测试
在两个进程轮转调度测试中,似乎由于时钟中断过于频繁,导致进程没有足够的运行时间,刚刚进入用户模式,连一条指令都没有执行就再次发生时钟中断,这种情况在bochs/virtualbox上有时会出现
在virtualbox中单个进程进行测试时,不会出现#GP异常,而加入多个进程测试时会出现#GP异常,出现#GP异常之前,已经至少轮转调度了一次进程,eip指出异常发生在trapret的iretd处,错误代码指出GDT访问越界,此现象在bochs中却没有
有时候频繁出现39号中断,有时仅出现几次
需要提供的接口:
构造第一个进程:
调用函数分配PCB,该函数同时完成PCB中一部分信息的初始化;
创建第一个进程的内核地址空间;
调用函数,初始化第一个进程的用户地址空间;
设置PCB.pgdir;
所有PCB.trapframe成员清零;
将PCB.trapframe.ss设置为引用用户模式数据段;
将PCB.trapframe.esp设置用户栈栈顶位置;
设置PCB.trapframe.eflags,开启中断,设置其IOPL为0,其余位为0或者保留值;
设置PCB.trapframe.cs/eip使其指向用户空间中进程执行的起始位置;
设置PCB.trapframe.gs/fs/es/ds,使其引用用户模式数据段;
设置PCB.name为initcode;
清空PCB.open_files中各个成员;
设置PCB.size为第一个进程镜像大小(不包括用户栈);
设置PCB.parent为NULL;
先将TSS所有成员清零,包括I/O映射基址
将TSS.ss0设置为内核模式数据段描述符的选择子;
将TSS.esp0设置为PCB.kstack的栈顶位置;
加载TSS选择子到TR;
设置PCB.state为RUNNABLE;
创建一个当前进程的副本作为其子进程:
成功将在系统中创建一个当前进程的副本进程为其子进程,父子进程在用户模式下的执行是连续的,在父进程中返回子进程的PID,在子进程中返回0;失败则返回-1;
调用函数,分配一个PCB给子进程使用,结果保存为child;
如果无PCB可用
返回-1;
将子进程的pid保存在pid中;
调用函数,创建当前进程地址空间的一个副本,结果保存在child->pgdir中;
如果child->pgdir为NULL
调用函数,释放child的内核栈;
关闭中断;
调用函数,清零该PCB;
将该PCB的状态置为UNUSED;
恢复原先的中断状态;
返回-1;
设置child->parent,使得该进程为当前进程的子进程;
将当前进程名作为child的进程名;
将当前进程的trapframe复制到child的内核栈顶;
复制当前进程的cwd引用,作为child的当前工作目录;
复制当前进程的打开文件列表给child,使其共享相同的打开文件结构;
将当前进程对应PCB.size作为child->size的值;
更改child->tf->eax,使child从内核中返回后的值为0;
关闭中断;
将child的状态置为RUNNABLE;
恢复原先的中断状态;
返回pid;
注意:
在当前设计中,pid使用uint32_t类型保存,但是限制pid只能在int32_t所能表示最大值范围内;
结束当前进程,保存退出状态以供父进程使用:
ret: 退出状态值;
成功后释放当前进程所占据的部分资源,并使其进入ZOMBIE状态,不可再次运行,失败则PANIC;
将ret保存在当前进程对应的PCB中;
调用函数,丢弃所有对打开文件结构的引用,清空打开文件指针列表;
调用函数,丢弃对当前工作目录对应i结点的引用,清空该指针;
调用函数,释放掉当前进程的用户地址空间内容,将PCB.size置为0;
关闭中断;
调用函数,通过父进程对应的PCB地址唤醒父进程;
遍历PCB表
如果发现一个有效PCB且该PCB对应进程为当前进程的子进程
设置该子进程的父进程为初始进程;
如果该子进程为ZOMBIE
调用函数,通过初始进程对应的PCB地址唤醒初始进程;
设置当前进程状态为ZOMBIE;
调用函数,切换到scheduler进程,不再返回;
等待子进程结束,并获取其退出状态:
ret: 指向保存退出状态值的位置;
如果有子进程结束则释放其所占据的剩余资源,返回其pid,并保存其退出状态到*ret中;如果所有子进程都尚未结束则睡眠下去直到某个子进程结束执行;如果没有子进程则返回-1;
关闭中断;
repeat处:
设置havekids为0;
遍历进程表,寻找其子进程;
如果当前PCB无效,或者所对应进程不是当前进程的子进程
跳过余下部分的处理,检查下一个PCB;
设置havekids为1;
如果当前进程的状态为ZOMBIE
设置*ret为该子进程的返回值;
设置pid为该子进程的pid;
调用函数,释放该子进程的内核栈;
调用函数,释放该子进程的分页结构(内核地址空间所映射的内存不会被释放);
调用函数,清零该子进程对应的PCB;
将该PCB状态置为UNUSED;
恢复原先的中断状态;
返回pid;
如果havekids为0
说明没有子进程;
恢复原先的中断状态;
返回-1;
调用函数,睡眠在当前进程对应的PCB上,等待某个子进程退出;
被唤醒后,跳转到repeat处,再次检查子进程的状态;
调度函数:
暂时开启中断;
关闭中断;
使用proc变量从头遍历PCB表
如果当前proc->state不为RUNNABLE
跳过循环中剩余部分;
切换到proc使用的分页结构中;
重新设置CPU.TSS.esp0为proc的内核栈栈顶位置;
设置proc->state为RUNNING;
设置CPU.cur_proc为proc;
调用swtch,将当前scheduler内核线程的上下文保存,切换到proc->context,此时proc对应的进程开始运行;
从swtch返回后,表明已经回到scheduler线程中了;
切换回scheduler线程的分页结构;
清空CPU.cur_proc;
返回,暂时的开关中断,再从头遍历PCB表;
swtch例程(用于内核线程的上下文切换,应该从C中调用):
使用push,在当前内核线程栈中保存其上下文;
更新当前PCB.context指针,使其指向栈上的内容;
切换esp到新内核线程PCB.context,即切换到其内核栈上;
使用pop,恢复新内核线程栈中保存的上下文;
使用ret,返回到新内核线程中执行;
分配PCB函数:
关闭中断;
从头遍历PCB表
如果当前PCB.state为UNUSED的
将当前PCB.state设置为NEWBORN;
恢复原先的中断状态;
设置当前PCB.pid为最小可用的pid;
记录该PCB的地址到new_pcb中;
跳出循环;
如果没有找到一个UNUSED的PCB
恢复原先的中断状态;
返回NULL;
清零eflags_stack,设置eflags_sp为嵌套次数最大值;
初始化channel为NULL;
初始化retval为0;
分配一个清零过的内核栈,写入new_pcb->kstack;
在这个内核栈中为trap frame留出空间;
使new_pcb->trapframe指向这个trap frame;
在内核栈中写入trapret的入口地址;
在内核栈上构建context;
使new_pcb->context指向这个context;
清空context中所有成员;
使context.cs/eip指向forkret的入口地址;
设置context.eflags,开启中断,设置其IOPL为0,其余位为0或者保留值;
返回new_pcb;
分配PCB函数,但是只应该在进程不可使用pushcli/popcli时被使用【注意使用方式】
函数执行逻辑等同于上述函数,但是没有考虑锁住PCB表!
注意:在使用该函数时,要保证没有其他进程在使用PCB表;
forkret函数:
初始化第一个进程的当前工作目录为根目录;
ret,使用特意压入内核栈上的eip返回到trapret中;
trapret例程(该例程是alltraps中恢复中断线程现场信息的部分):
使用pop,恢复proc内核栈中的部分trap frame内容;
使用iret,恢复cs/eip/eflags/ss/esp,从中断中返回;
至此,proc从PCB.trapframe.cs/eip处开始执行;
嵌套的关闭和开启中断
参见daily_record.txt/20:58 2019/2/2
睡眠(channel):
保存原先的中断状态,并关中断
pushcli
必须存在当前进程
if(cpu.cur_proc == 0)
PANIC
在channel上睡眠
cpu.cur_proc.channel = channel
cpu.cur_proc.state = SLEEPING
切换到scheduler
swtch(&cpu.cur_proc.context, cpu.scheduler)
被唤醒后
cpu.cur_proc.channel = 0
恢复原先的中断状态
popcli
唤醒(channel):
保存原先的中断状态,并关中断,当前调用进程必须可调度
pushcli
调用函数,在关闭中断的情况下唤醒在channel上的进程
恢复原先的中断状态
popcli
已经关闭中断时的唤醒(channel):
遍历进程表,找出睡眠在channel上的进程,如果有则唤醒它
for(pcb = &pcb_table[0]; pcb != &pcb_table[N]; pcb++)
if pcb->state == SLEEPING && pcb->channel == channel
pcb->state = RUNNABLE
创建子进程【系统调用】:
成功将在系统中创建一个当前进程的副本进程为其子进程,父子进程在用户模式下的执行是连续的,在父进程中返回子进程的PID,在子进程中返回0;失败则返回-1;
调用函数,实现子进程的创建,并返回其返回值;
结束当前进程【系统调用】:
调用函数,获取用户栈中第0个4字节参数作为当前进程的退出状态,保存在retval中;
如果失败则PANIC;
调用函数,以retval为退出状态,结束当前进程;
等待子进程退出【系统调用】:
调用函数,获取用户栈中第0个4字节参数作为指针,保存在retval中;
如果失败则PANIC;
调用函数,等待子进程退出,并将其退出状态保存在retval指向的位置;
返回上述函数的返回值;
返回当前进程pid【系统调用】:
返回pid;
文件的组织结构:
process.h: 定义CPU相关数据结构、trapframe结构、context结构、PCB结构、TSS结构;
提供与process相关的函数原型、拓展在其他文件中的CPU结构体变量定义范围;
process.c: 提供与process相关的函数实现、定义与process管理相关的变量(CPU、PCB表);
process.asm: 提供与process相关的汇编例程;
sys_proc.c: 封装与进程管理有关的系统调用;
5.文件系统
文件系统用于持久化存储、管理数据,并对资源的并发访问提供支持。
文件系统在磁盘上的格式:
sector 0(LBA28地址) boot sector
sector 1 super block(超级块)
sector 2...n disk inode bitmap
sector n+1...m block bitmap
sector m+1...i disk inodes
sector i+1... blocks
每个扇区为512字节,每个block(块)亦为512字节,一一对应。
超级块中包含了文件系统的元信息,其格式如下:
block bitmap占用的块数,4字节;该bitmap用于记录文件系统中块的分配情况,boot sector、super block、disk inode bitmap以及block bitmap所占用的块不计入该位图内;
disk inode bitmap占用的块数,4字节;该bitmap用于记录文件系统中磁盘i结点的分配情况,boot sector、super block、disk inode bitmap以及block bitmap所占用的块不计入该位图内;
可用于分配的block总数,4字节;即block bitmap中有效位的个数;
可用于分配的磁盘i结点总数,4字节;即disk inode bitmap中有效位的个数;
磁盘i结点占用的块数,4字节;
disk inode从0开始编号,disk inode bitmap的第一个bit对应第0个disk inode,依次类推;block从0开始编号,block bitmap的第一个bit对应第0个block,依次类推,第0个block视为无效;如果两个bitmap中有些bit是无效的,则在构建文件系统时就应该将这些bit置位。
若已知bitmap中某bit相对于该bitmap开头的偏移offset,则可根据如下方法计算该bit对应的block/inode的编号以及所在sector号:
block编号=offset
block所在sector号 = 2 + 两个bitmap所占sector数 + disk inodes所占用的sector数 + offset
disk inodes所占用的sector数 = (可用于分配的磁盘i结点总数 * sizeof(disk inode)) % sizeof(block) ? (可用于分配的磁盘i结点总数 * sizeof(disk inode) / sizeof(block) + 1)
: (可用于分配的磁盘i结点总数 * sizeof(disk inode) / sizeof(block))
磁盘i结点编号=offset
disk inode所在sector号 = 2 + 两个bitmap所占sector数 + offset/(sizeof(block)/sizeof(disk inode))
disk inode所在sector中的偏移(即还需要偏移多少个inode) = offset % (sizeof(block)/sizeof(disk inode))
本文件系统分为块分配层、i结点层、目录层、路径层、打开文件层、文件描述符层来实现。块分配层负责分配、回收数据块;i结点层提供无名文件,每一个这样的文件由一个i结点和系列数据块组成;目录层在i结点层的基础上实现目录,即目录只是一种特殊的无名文件;路径层实现路径与i结点的映射;打开文件层维护文件使用过程的中的信息,且为文件系统管理的资源提供一层统一的抽象;文件描述符层则帮助隐藏了实现细节;
磁盘i结点用于记录文件的信息,每一个文件均有一个磁盘i结点和一些数据块组成,其格式如下:
类型,2字节;表示该磁盘i结点代表的是目录、普通文件还是设备(字符设备、块设备);目前不在磁盘i结点中记载文件的读写属性,默认可读可写,亦不做可执行权限检查;
主设备号、从设备号,各2字节;当该磁盘i结点表示设备文件时,使用这两个字段表示何种设备,主设备号指定设备类型而从设备号指定同一类型的某个设备;由于在buf cache中使用的dev为int32类型,而这里指定的设备号均为uint32类型,所以需要注意合法的设备号范围;
链接计数,2字节;记录有多少个目录项指向了该磁盘i结点;
文件大小,4字节;记录文件内容长度,以字节为单位,对设备文件没有定义;
记录数据块号的数组,每个元素4字节,共13个元素;0-11号元素为直接索引,即直接可以使用该索引号找到目标数据块,12号为间接索引,需要先通过该间接索引号找到一个间接索引块(数据块),再从该间接索引块中读取一个索引号,找到目标数据块;由于0号block不使用,所以索引0视为无效,便于对数组进行初始化。
磁盘i结点应该是2的次幂个字节,这样不会跨扇区。
在内核中构建了内存i结点,其内容为磁盘i结点的副本并添加了一些控制信息,其结构如下:
设备号,4字节,表示该i结点是从哪个设备上读出的;
i结点号,4字节,为disk inode bitmap中对应bit的偏移量;
引用计数,4字节,表示有多少对该i结点的引用(指针);
标志位,4字节,表示该i结点的使用状态;为0表示未设置任何标志;
BUSY,表示已经被某个进程锁住;
VALID,表示其内容有效(指磁盘i结点内容副本);
磁盘i结点内容副本;
同时,内核提供一个inode cache(由多个内存i结点结构组成,目前暂设50个);类似于buf cache,inode cache用于同步多进程对inode的访问:inode可以被多个进程同时引用,但是同一时间,只能有一个进程锁住一个inode为自己使用(BUSY),使用时应该避免在锁住一个inode后再想锁住另一个inode,这样可能导致死锁。
inode cache只缓存正在被引用的inode,若cache中某个inode引用计数为0,表明该inode结构为空的,即可重新装填新的磁盘i结点;一个磁盘i结点同时只能在cache中拥有一个副本;当引用计数大于0时,其设备号、i结点号是有效的,当标志为VALID时,该inode结构中拥有对应磁盘i结点有效的内容副本。
使用新的磁盘i结点前需要进行分配,分配到的磁盘i结点将与inode cache中的一个inode对应起来且其引用计数将加1,进程将获得该inode的指针;如果进程需要对该磁盘i结点进行修改,则需要对该inode(或者说是该磁盘i结点)上锁(BUSY);上锁中若发现该inode数据无效(UN-VALID),则读取磁盘i结点数据到该inode中;进程修改后应该主动同步该inode的内容到磁盘上;进程使用完毕后应该解锁该inode,以便其他进程使用,解锁仍然保持进程对该inode的引用;若进程不再需要该inode,则应该丢弃该inode的引用,丢弃过程中会检查该inode的引用计数,倘若引用计数为0,则这个inode将被标明空闲,若同时其链接计数也为0,则还需要在磁盘上释放该磁盘i结点和所关联的数据块。
若进程想要使用的某个磁盘i结点已经缓存在cache中了,则可以直接返回对应inode的指针;如果没有缓存在cache中,则会从中找一个空闲的inode结构来缓存该磁盘i结点。
在inode层实现了统一的读写函数,便于访问inode表示的资源(目录、设备、普通文件)。
用户进程可以通过stat结构获取inode的信息,其定义如下:
inode所在设备号;
inode号;
inode类型,目录、普通文件、设备;
inode所表示的设备号(主、从设备号,仅当inode表示设备时有效);
指向该inode的链接数;
inode所表示的普通文件大小(字节单位);
应该通过正常的途径获取i结点编号,在多进程竞争的情况下直接使用i结点编号(比如直接通过指定设备号和i结点号来获取inode指针)有风险,可能会破坏文件系统的结构或者导致死锁。
在当前的设计中,I/O设备也被抽象为一个设备文件,可以通过一个i结点文件来对它进行操纵。i结点的类型、主从设备号用来确定是其所表示的是哪个设备。
针对字符类设备,系统维护有一个设备表,每一个设备均具有一个对应的表项;表项中则注册有用于操纵该设备的函数;可通过主设备号索引到该设备在设备表中的表项。目前针对字符类设备仅支持读写两种操作;且仅支持1个字符类设备。
字符类设备表的表项定义如下:
读函数:
从设备读取n个字节到缓冲区buf中,允许读取的字节数少于n;成功返回实际读取的字节数,失败返回-1;
写函数:
将缓冲区buf中的n个字节写入设备,如果写入失败则返回-1,否则应该返回实际写入的字节数;
目录也是通过i结点来构建的,若i结点所表示的资源内容由若干个目录项组成,则称该i结点表示一个目录(DIR_INODE);目录项包括一个i结点号和与之关联的名字,由于可以有多个目录项指向同一个i结点(链接计数),所以一个i结点可以有多个关联的名字;一个目录项的名字最长为28个字节,如果少于28个字节,则在该名字的后面添加一个NUL字符表示结尾;一个空名字的目录项被视为空的(目录项的名字的第一个字符为NUL);一个目录文件中可以包含多个指向子目录或者文件的目录项,但这些目录项不能同名;每一个目录中均包含名字为“.”和“..”的特殊目录项,分别指向当前目录和当前目录的上层目录;根目录中的“..”则指向其自身。
当目录中仅含有“.”和“..”这两个目录项时,则认为该目录是空的。
用于保存系统启动所必需文件的文件系统被称为根文件系统,其所在设备被称为根设备,目前由于仅支持一个IDE硬盘,所以根设备号为0。
设定根目录所在i结点号为0。
文件系统中通过路径来指定所要访问的资源,路径名即是资源名;路径名由分隔符'/'和元素构成,元素指目录名或者文件名;设计中不对路径的最大层次做限制(层次指路径中元素的个数)。
以'/'开头的路径被称为绝对路径,对这类路径的解析总是从根目录开始;非'/'开头的路径被称为相对路径,其解析则是从进程当前工作目录开始。
路径中一个或者多个连续的'/'均被视为一个'/',作为路径中元素的分隔符;其余的连续字符则构成路径中的多个元素,以分隔符分隔开来;元素长度(组成元素的字符个数)不能超过目录项名字的最大长度,在解析过程中超过部分被忽略;除路径最后一个元素外,其余元素必须是目录名。
例如:
“//ab///c/d//”为绝对路径,等同于“/a/c/d”,这里假设最长目录名为一个字符;
“./aa//c/d”为相对路径,等同于“./a/c/d”,这里假设最长目录名为一个字符;
“./a/../c”为相对路径,等同于“./c”,这里假设最长目录名为一个字符;
针对每一个打开的inode(目录、普通文件、设备)或者管道,都有一个open_file_struct与之对应,除了inode信息,还记录文件或管道在使用过程中的一些信息,比如文件偏移量、文件的操作模式(读写)、多少个文件描述符引用了这个结构,这些信息被视为使用过程中的信息;其具体结构定义如下:
类型,该打开文件结构所表示的资源类型,目前支持两类:inode文件、管道
引用计数,2字节,这个结构的文件描述符引用计数;
操作模式,4字节,比如只读、只写、读写;
inode指针,所描述的目标i结点;
管道指针,所描述的管道对象;
文件偏移,4字节,相对于文件开头的偏移量,读、写从这个位置开始,目前只对目录和普通文件有意义;
内核维护一个全局的open_file_struct列表,表示当前系统中所有正在使用中的open_file_struct;可以有多个open_file_struct关联同一个inode或者管道,这样就允许存在多个inode使用实例;每个进程都维护有一个open_file_struct指针列表,文件描述符是这个指针列表的索引,当文件描述符对应的指针不为NULL,则称这个文件描述符引用了一个open_file_struct,而当其为NULL时,表示这个文件描述符可被重新使用;一个进程可以有多个文件描述符引用同一个open_file_struct,也可以不同进程间同时拥有引用同一个open_file_struct的文件描述符;open_file_struct的引用计数记录当前被引用的次数,当引用计数为0时,表示该结构空闲可被回收利用。
目前暂定内核支持最大同时使用64个open_file_struct,每个进程能够同时拥有最多20个open_file_struct结构引用;
open_file_struct在使用中,其类型、操作模式、inode指针、管道指针均不会再改变,所以设计上没有在给这个结构添加类似inode的锁标志;当需要修改引用计数时则采取暂时关闭中断的方式保证操作的原子性;对于文件偏移,可能存在多个进程共享同一个open_file_struct时,同时操作文件偏移;对于读、写操作而言,在修改文件偏移时需要先获得inode锁,以保证同时只有一个进程进行操作,同时对一个文件写并不会覆盖各自的文件,但是写的顺序是不被保证的,因此写的结果可能是交织的(在一个写操作的过程中插入了另一个写操作);对于文件偏移定位(比如linux中的lseek)这种操作,则需要进程自身提供同步的方式以保证其正确性;
块分配层:
block bitmap中的每一个bit代表数据区中一个block的使用情况(置位表示已使用,否则表示空闲),其偏移量即为block的编号。
提供的功能:
读取指定块设备上的文件系统的超级块;
清空指定block中的内容;
在指定块设备上的文件系统中分配空闲块,返回其编号:
读取文件系统的super block;
根据super block的信息,搜索block bitmap,寻找空闲位;
设置该空闲位,更新到磁盘;
清零该空闲位对应的block,然后返回该block的编号;
未找到则PANIC;
在指定块设备上的文件系统上回收指定block:
读取文件系统的super block;
根据super block的信息,搜索block bitmap,寻找该block对应的位;
如果该位被置位则PANIC;
重置该位,更新到磁盘;
文件的组织结构:
block.h:提供块分配函数接口和超级块结构定义;
block.c:提供块分配函数实现;
fs.h:提供文件系统相关的结构定义;
i结点层:
类似于block bitmap,disk inode bitmap中的每一个bit代表一个disk inode的使用情况,其偏移量为i结点的编号。
在指定设备的文件系统上分配磁盘i结点,并返回其对应的内存inode的引用(设备号dev):
读取dev上的文件系统的超级块;
根据超级块定位到disk inode bitmap;
遍历disk inode bitmap所占据的block;
如果发现一个空闲bit
置位,写回磁盘;
释放对block的锁;
根据dev和i结点编号,调用函数在inode cache中查找或者新分配一个inode结构,并返回其指针;
释放对block的锁;
查找失败,PANIC;
在指定设备的文件系统上释放磁盘i结点(设备号dev,i结点编号):
读取dev上的文件系统的超级块;
根据超级块、i结点编号定位到disk inode bitmap中的某个block;
读取并锁住该block;
如果该block中该i结点对应的bit没有置位则PANIC;
清除该block中该i结点对应的bit;
写回设备;
释放该block;
在inode cache中查找或者新分配一个对应于指定设备上某个i结点的inode结构(设备号,i结点编号):
关闭中断;
遍历inode cache
如果找到设备号、i结点编号对应的inode结构且其引用计数大于0
说明该磁盘i结点已被cache;
增加其引用计数;
恢复原先的中断状态;
返回其指针;
记录找到的第一个引用计数为0的inode结构;
如果找到一个引用计数为0的inode结构
设置其设备号和i结点编号;
设置引用计数为1;
清空标志位;
恢复原先的中断状态;
返回指针;
否则PANIC
注意:这里没有检查设备号和i结点编号
对inode上锁,需要时从磁盘获取该inode对应的数据(inode指针):
如果inode引用计数小于1,则PANIC;
关闭中断;
如果inode为BUSY的
睡眠在该inode上,等待被唤醒;
返回继续检查inode的状态;
在该inode的状态中添加BUSY标志;
恢复原先的中断状态;
如果inode不是VALID的
读取并锁住inode对应磁盘i结点所在block;
将block中对应的数据复制到inode中;
释放block上的锁;
在inode状态中添加VALID标志;
将inode中的磁盘i结点内容副本写入磁盘(inode指针):
如果inode引用计数小于1或者是UN-BUSY的,则PANIC;
读取inode所在dev上的文件系统的超级块;
根据超级块定位到inode所在的block;
读取并锁住block;
将inode更新到block上相应位置;
写回block;
释放该block;
注意:这里在锁住inode的同时还请求锁住超级块,但是由于我们在获得超级块后立即将其释放,所以目前来看不会造成死锁,下一步的做法可以考虑在第一个进程从forkret返回时,调用函数将超级块读入,然后所有进程共享这一个副本,只读入一次即可;或者是提供一个忙等待的驱动接口,在内核初始化时直接读取文件系统的超级块;
解锁inode(inode指针):
如果inode是UN-BUSY的或者其引用计数小于1,则PANIC;
清除inode中的BUSY位;
唤醒在该inode上睡眠的进程;