├── .gitignore ├── .mkbok.yml ├── .travis.yml ├── 0101pdf.jpg ├── BUILD.md ├── README.md ├── Rakefile ├── SUMMARY.md ├── book.json ├── build ├── codes ├── file-helloworld.c ├── getchar-helloworld.c ├── helloworld.c └── timing-helloworld.c ├── convert-to-utf8.sh ├── epub ├── book.css └── metadata.xml ├── figures-source ├── chap03-git-dvcs.bmml ├── chap07-cucumber-arch.bmml └── cover.bmml ├── figures ├── 18333fig0201-tn.png └── chapt1-ucore-arch.png ├── id_rsa_mkbok ├── latex ├── .gitignore ├── ISR_in_ucore.tex ├── Makefile ├── README.md ├── access_harddisk.tex ├── appendix.tex ├── boot.tex ├── config.yml ├── create_exec_user_process.tex ├── create_exec_user_thread.tex ├── create_kern_thread.tex ├── create_kern_thread_.tex ├── create_user_process.tex ├── create_user_thread.tex ├── deadlock.tex ├── design_PCB.tex ├── elf_format.tex ├── filesystem.tex ├── gbt7714-2005.bst ├── handle_pages_fault.tex ├── hardware.tex ├── hardware_intr.tex ├── helloworld.tex ├── implement_copy_on_write.tex ├── implement_pages_mem_managment.tex ├── implement_rand_mem.tex ├── implement_shared_mem.tex ├── implement_swap.tex ├── init_IDT.tex ├── init_intr_controller.tex ├── init_intr_in_device.tex ├── intro.tex ├── io_access.tex ├── ipc.tex ├── kernel_function_call_stack.tex ├── kernel_to_user.tex ├── load_run_ucore.tex ├── makepdf ├── memory.tex ├── os_abstract.tex ├── os_define.tex ├── os_feature.tex ├── os_history.tex ├── os_interface.tex ├── osprinciple_control_computer.tex ├── overlook.tex ├── page_alloc_algorithm.tex ├── page_fault_VMA.tex ├── pages_managment.tex ├── pages_mem_managment.tex ├── pages_replacement_based_on_kern_process.tex ├── pages_replacement_excution.tex ├── phymem_analysis.tex ├── phymen_size.tex ├── poweron.tex ├── prerequisite.tex ├── privilege_level.tex ├── process.tex ├── process_exit_wait.tex ├── process_managment.tex ├── process_schedule.tex ├── process_schedule_implement.tex ├── process_schedule_principal.tex ├── process_status_change.tex ├── process_summary.tex ├── proj1_small_bootloader.tex ├── proj2_bootloader_load_ucore.tex ├── proj3_function_call_stack.tex ├── proj4_1_1_kernel_switch_to_user.tex ├── proj4_intr_in_ucore.tex ├── proj7_8_9.tex ├── protect_mode.tex ├── real_mode_switch_protect_mode.tex ├── reference.tex ├── riscv.tex ├── rv_pages_hardware.tex ├── sample.bib ├── sample.tex ├── schedule.tex ├── setup_stack.tex ├── setuplinuxenv.tex ├── setupmammalcomputerenv.tex ├── show_string.tex ├── show_string_in_ucore.tex ├── slab.tex ├── sosbook.bib ├── sosbook.tex ├── stack_process.tex ├── startingos.tex ├── summary.tex ├── support_swap.tex ├── support_virtual_mem_managment.tex ├── swap_algorithm.tex ├── syncmutex.tex ├── syscall_implement.tex ├── task_switch.tex ├── template.tex ├── thread.tex ├── ucore.tex ├── ucore_code.tex ├── ucore_control_computer.tex ├── user_to_kernel.tex ├── virtual_mem_managment.tex ├── vm.tex ├── vma_struct.tex ├── wait_awaken_based_time_event.tex ├── wait_quence_implement.tex ├── what_is_process.tex ├── what_is_thread.tex ├── what_is_user_process.tex ├── x86_pages_hardware.tex ├── zhbook.dtx └── zhbook.ins ├── makeebooks ├── makepdfs ├── mkbok ├── package.json ├── sample ├── codeminimal.tex └── minted.sty ├── summary.rb ├── template.html ├── title.zh.txt ├── upload.sh └── zh ├── .gitignore ├── chapter-1 ├── access_harddisk.md ├── bootloader_up_os.md ├── elf_format.md ├── figures │ ├── 3.13.1.png │ ├── 3.13.2.png │ ├── 3.13.3.png │ ├── 3.13.4.png │ ├── 3.13.5.png │ ├── 3.15.1.png │ ├── 3.15.2.png │ ├── 3.15.3.png │ ├── 3.18.1.png │ ├── 3.2.4.1.png │ ├── 3.2.4.2.png │ ├── 3.2.7.1.png │ ├── 3.2.7.2.png │ ├── os-position.png │ └── qemu_cha1.jpg ├── io_access.md ├── load_run_ucore.md ├── poweron.md ├── proj1_small_bootloader.md ├── proj2_bootloader_load_ucore.md ├── protect_mode.md ├── real_mode_switch_protect_mode.md ├── reference.md ├── setup_stack.md ├── show_string.md ├── show_string_in_ucore.md ├── summary.md └── ucore_code.md ├── chapter-2 ├── ISR_in_ucore.md ├── figures │ ├── 3.3.3.1.png │ ├── 3.4.2.1.png │ ├── 3.4.3.1.png │ ├── 3.4.3.2.png │ ├── 3.4.3.3.png │ ├── 3.4.3.4.png │ ├── 3.4.3.5.png │ ├── 3.4.7.1.png │ ├── 3.4.7.2.png │ ├── 3.4.7.3.png │ ├── 3.5.2.1.png │ └── 3.5.5.1.png ├── hardware_intr.md ├── init_IDT.md ├── init_intr_controller.md ├── init_intr_in_device.md ├── kernel_function_call_stack.md ├── kernel_to_user.md ├── osprinciple_control_computer.md ├── privilege_level.md ├── proj3_function_call_stack.md ├── proj4_1_1_kernel_switch_to_user.md ├── proj4_intr_in_ucore.md ├── stack_process.md ├── summary.md ├── task_switch.md ├── ucore_control_computer.md └── user_to_kernel.md ├── chapter-3 ├── figures │ ├── 1.png │ ├── 10.png │ ├── 11.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ └── 9.png ├── handle_pages_fault.md ├── implement_copy_on_write.md ├── implement_pages_mem_managment.md ├── implement_rand_mem.md ├── implement_shared_mem.md ├── implement_swap.md ├── page_fault_VMA.md ├── pages_algors.md ├── pages_managment.md ├── pages_mem_managment.md ├── phymem_analysis.md ├── phymen_size.md ├── proj7_8_9.md ├── slab.md ├── support_swap.md ├── support_virtual_mem_managment.md ├── swap_algors.md ├── ucore_phymem_management.md ├── virtual_mem_managment.md ├── vma_struct.md └── x86_pages_hardware.md ├── chapter-4 ├── create_exec_user_process.md ├── create_exec_user_thread.md ├── create_kern_thread.md ├── create_kern_thread_.md ├── create_user_process.md ├── create_user_thread.md ├── design_PCB.md ├── figures │ ├── 0.png │ ├── 0_2.png │ ├── 0_3.png │ ├── 0_4.png │ ├── 0_5.png │ ├── 1.png │ ├── 2.png │ ├── 3.png │ └── 4.png ├── pages_replacement_based_on_kern_process.md ├── pages_replacement_excution.md ├── process_exit_wait.md ├── process_managment.md ├── process_schedule.md ├── process_schedule_implement.md ├── process_schedule_principal.md ├── process_status_change.md ├── process_summary.md ├── syscall_implement.md ├── wait_awaken_based_time_event.md ├── wait_quence_implement.md ├── what_is_process.md ├── what_is_thread.md └── what_is_user_process.md ├── cover └── cover.md ├── md2tex.sh ├── preface ├── .md ├── cpu.md ├── figures │ ├── io_arch.png │ ├── mem_arch.png │ ├── os_interface.png │ ├── oslab-chapt1-106125.png │ ├── oslab-chapt1-106501.png │ ├── oslab-chapt1-106722.png │ ├── oslab-chapt1-106926.png │ ├── pc_arch.png │ ├── software-stacks.png │ └── ucore_arch.png ├── hardware.md ├── io.md ├── memory.md ├── memory │ └── .md ├── osabstract.md ├── osconcept.md ├── osfeature.md ├── oshistory.md ├── osinterface.md ├── pc.md ├── preface.md ├── preknowledge.md ├── riscv.md ├── smallos_ucore.md ├── summary_preface.md ├── ucore.md └── understandos.md └── supplement ├── copyright.md ├── mooc-os.md ├── supplement.md ├── ucore-contributors.md ├── ucore-history.md ├── ucore-projlists.md └── ucore-tools.md /.gitignore: -------------------------------------------------------------------------------- 1 | kaiyuanbook.* 2 | _book 3 | _book/* 4 | *.pdf 5 | *.swp 6 | *.aux 7 | *.log 8 | epub/temp 9 | *~ 10 | .#* 11 | *.epub 12 | node_modules/ 13 | -------------------------------------------------------------------------------- /.mkbok.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ucoreos 3 | build: pdf 4 | lang: zh 5 | license: ccbyncnd30 6 | config: latex/config.yml 7 | template: latex/template.tex 8 | preface-files: preface/*.markdown 9 | chapter-files: chapters/*.markdown 10 | appendix-files: appendix/*.markdown 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | before_script: 3 | - uname -a 4 | - lsb_release -a 5 | - sudo apt-get install pandoc 6 | # here you can check os environment, it is ubuntu natty 7 | - sudo apt-get install ttf-arphic-gbsn00lp ttf-arphic-ukai ttf-wqy-microhei ttf-wqy-zenhei 8 | - sudo apt-get install texlive-xetex texlive-latex-recommended texlive-latex-extra 9 | - pandoc -v 10 | - gem install mkbok 11 | 12 | rvm: 13 | - 1.9.3 14 | script: 15 | # - mkbok -v ; mkbok 16 | - chmod +x mkbok 17 | - ./mkbok 18 | 19 | after_script: 20 | - chmod +x upload.sh 21 | - which bash 22 | - ls -al 23 | - ./upload.sh 24 | 25 | branches: 26 | only: 27 | - master 28 | 29 | 30 | -------------------------------------------------------------------------------- /0101pdf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/0101pdf.jpg -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- 1 | # Introduction # 2 | 3 | As open source books, ebooks and pdf format should be created on fly, the following sections describe those solution in detail. 4 | 5 | The solution below is based on [Pro Git][progit]; while it is little updated on format inside. 6 | 7 | # Making Pdf books # 8 | PDF format is used to read/print in nice way like real book, [pandoc][pandoc] good at this and it is used instead to generate latex from markdown, and latex tool `xelatex` (is part of [TexLive][texlive] now) is used to convert pdf from latex. 9 | 10 | Please check [ctax](http://www.ctan.org/) and [TexLive][texlive] for more background for latex, which is quite complicated and elegant if you have never touched before. 11 | 12 | ## Ubuntu Platform ## 13 | 14 | Ubuntu Platform Oneiric (11.10) is used mainly due to pandoc. 15 | 16 | [pandoc][pandoc] can be installed directly from source, which version is 1.8.x. If you use Ubuntu 11.04, then it is just 1.5.x. 17 | 18 | Though texlive 2011 can be installed separately, the default one texlive 2009 from Ubuntu repository is good enough so far. 19 | 20 | $ sudo apt-get install ruby1.9.1 21 | $ sudo apt-get install pandoc 22 | $ sudo apt-get install texlive-xetex 23 | $ sudo apt-get install texlive-latex-recommended # main packages 24 | $ sudo apt-get install texlive-latex-extra # package titlesec 25 | 26 | You need to install related fonts for Chinese, fortunately they exist in ubuntu source also. 27 | 28 | $ sudo apt-get install ttf-arphic-gbsn00lp ttf-arphic-ukai # from arphic 29 | $ sudo apt-get install ttf-wqy-microhei ttf-wqy-zenhei # from WenQuanYi 30 | 31 | Then it should work perfectly 32 | 33 | $ ./mkbok 34 | 35 | Just remind you, some [extra pandoc markdown format](http://johnmacfarlane.net/pandoc/README.html) is used inside this book: 36 | 37 | * code syntax highlight (doesn't work in pdf, while it should work in html/epub which needed later) 38 | * footnote 39 | 40 | [pandoc]: http://johnmacfarlane.net/pandoc/ 41 | [calibre]: http://calibre-ebook.com/ 42 | [progit]: http://github.com/progit/progit 43 | [texlive]: http://www.tug.org/texlive/ 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 操作系统的基本原理与简单实现 3 | =============== 4 | ## *--基于ucore OS + RISC-V* 5 | 6 | 对于在校的学生和已经参加工作的工程师而言,能否以较小的时间和精力比较全面地了解操作系统呢?陆游老夫子说过“纸上得来终觉浅,绝知此事要躬行”,也许在了解基本的操作系统概念和原理基础上,通过实际动手来一步一步分析、设计和实现一个微型化的操作系统,会发现操作系统原来如此,概念原理和实际实现之间有紧密的联系和巨大的差异。 7 | 8 | 早期开放开源的UNIX操作系统和MIT教授 Frans Kaashoek 等基于UNIX v6设计的xv6操作系统给了我们启发:对一个计算机专业的本科生而言,设计实现一个操作系统有挑战但是可行!但x86相对封闭&复杂和有一定历史包袱的CPU硬件接口给OS学习带来了一定的挑战。1980年前后,UC Berkeley的Dave Patterson主导了Berkeley RISC项目并设计了其第一代的处理器RISC I,并在2014年发展到了开放&开源的第五代指令集架构RISC-V。本书想进行这样的教学尝试,以操作系统基本原理为教学引导,以简洁的RISC-V CPU为底层硬件基础,设计并实现一个微型但全面的“麻雀”操作系统—ucore。期望能够采用简化的计算机硬件为基础,以操作系统的基本概念和核心原理为实践指导,逐步解析操作系统各种知识点和对应的实验,做到有“理”可循和有“码”可查,最终让读者了解和掌握操作系统的原理、设计与实现。 9 | 10 | 陈渝 向勇 11 | 12 | 清华大学计算机系 13 | 14 | 2018年春 15 | 16 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "introduction": { 3 | "path": "README.md", 4 | "title": "介绍" }, 5 | "plugins": [ 6 | "mathjax","duoshuo", "quizzes", "-lunr", "-search", "search-pro" 7 | ], 8 | 9 | "pluginsConfig": { 10 | "mathjax": { 11 | "forceSVG": true 12 | }, 13 | "duoshuo": { 14 | "short_name": "mytest", 15 | "theme": "default" 16 | }, 17 | "quizees":{} 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /build: -------------------------------------------------------------------------------- 1 | rm -rf ./latex/zh 2 | rm -f ucoreos.zh.pdf 3 | ./mkbok && okular ucoreos.zh.pdf 4 | -------------------------------------------------------------------------------- /codes/file-helloworld.c: -------------------------------------------------------------------------------- 1 | #include 2 | void main(void){ 3 | FILE *fp; 4 | fp = fopen("file-helloworld.txt", "w"); 5 | fputs("hello world!",fp); 6 | fclose(fp); 7 | } 8 | -------------------------------------------------------------------------------- /codes/getchar-helloworld.c: -------------------------------------------------------------------------------- 1 | void main(void) 2 | { 3 | char name[100]; 4 | int i=0; 5 | while((name[i] = getchar())!='\n' && i<80) 6 | i++ ; 7 | name[i]=0; 8 | strcat(name,", hello world!"); 9 | puts(name); 10 | } 11 | -------------------------------------------------------------------------------- /codes/helloworld.c: -------------------------------------------------------------------------------- 1 | void main(void) 2 | { 3 | puts("hello world!"); 4 | } 5 | -------------------------------------------------------------------------------- /codes/timing-helloworld.c: -------------------------------------------------------------------------------- 1 | void main(void) 2 | { 3 | while(1) { 4 | sleep(1); 5 | puts("hello world!"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /convert-to-utf8.sh: -------------------------------------------------------------------------------- 1 | iconv -f GB2312 -t UTF-8 gb1.txt >gb2.tt 2 | -------------------------------------------------------------------------------- /epub/book.css: -------------------------------------------------------------------------------- 1 | ul { 2 | margin: 20px; 3 | } 4 | 5 | ol { 6 | margin: 20px; 7 | } 8 | 9 | body pre { 10 | margin: 10px; 11 | font-weight: bold; 12 | } 13 | 14 | body pre2 { 15 | background-color: silver; 16 | padding: 10px; 17 | border-top-style: solid; 18 | border-left-style: solid; 19 | border-left-width: 1px; 20 | border-top-width: 1px; 21 | overflow-x: auto; /* Use horizontal scroller if needed; for Firefox 2, not needed in Firefox 3 */ 22 | white-space: pre-wrap; /* css-3 */ 23 | white-space: -moz-pre-wrap !important; /* Mozilla, since 1999 */ 24 | white-space: -pre-wrap; /* Opera 4-6 */ 25 | white-space: -o-pre-wrap; /* Opera 7 */ 26 | /* width: 99%; */ 27 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 28 | } -------------------------------------------------------------------------------- /epub/metadata.xml: -------------------------------------------------------------------------------- 1 | Creative Commons Non-Commercial Share Alike 3.0 2 | zh 3 | -------------------------------------------------------------------------------- /figures-source/chap07-cucumber-arch.bmml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | right 8 | Cucumber 9 | 10 | 11 | 12 | 13 | bottom 14 | true 15 | 16 | 17 | 18 | 19 | 20 | 10066329 21 | 22 | 23 | 24 | 25 | true 26 | 16 27 | disabled 28 | %u88AB%u6D4B%u7CFB%u7EDF 29 | 30 | 31 | 32 | 33 | center 34 | %u9A71%u52A8%u5C42%0A%28_ruby%2C%20java_%29 35 | 36 | 37 | 38 | 39 | center 40 | %u4E1A%u52A1%u5C42%0A%28_gherkin_%29 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /figures-source/cover.bmml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16777215 6 | 72 7 | Software%20Development 8 | 9 | 10 | 11 | 12 | 0.75 13 | 0 14 | 545684 15 | 16 | 17 | 18 | 19 | 0.75 20 | 0 21 | 545684 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | true 32 | 28 33 | Learning%20in%204%20days 34 | 35 | 36 | 37 | 38 | 6710886 39 | true 40 | 32 41 | Larry%20Cai 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /figures/18333fig0201-tn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/figures/18333fig0201-tn.png -------------------------------------------------------------------------------- /figures/chapt1-ucore-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/figures/chapt1-ucore-arch.png -------------------------------------------------------------------------------- /id_rsa_mkbok: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAo2b2oZ8eZ5I1X6jwrTAJVo5qhRrwthwbs7xZVS/N3RcBK4tY 3 | I+5gPnda+RHaYZOr+GDAAH1MX9uOu59njmsD3zvaIVkgJN9nUIJAe1ETm+xqvkOq 4 | IyUHkV+gimHzlfOnhQo3V89MyeSAa9uNtsIluE7VMcyWKRlTzTyDUSKNLTtQs9L/ 5 | 16MIzMdQbppV/lxayt81acRHv4BAhBc+7AaoZxGvsDIi6yB5FQZec7eYlHcSobU3 6 | JO1CELS9pngORPV8QMbctWmdLyA4vgSjbHZRcBwWFQQ1AcAto1+PWsKm0r7r71Qj 7 | Pt7vb1N6X7/wprZWs1VFgwSzyykDdK2gdiYVrQIDAQABAoIBAHeQuPlMqH/a4YlM 8 | 4ken9b1LWrBpJSfsa2zabnfCEh4sXiPRw74PCIfM6FmGcciiuNx3VydEiCVAUDIE 9 | E0+ICP5JGnT6dWXlvwCPPsW9bt7SYM02UAGFcAM0+REpWYcpEjRsZFCxlJvKwxmp 10 | PwozCEzUCnZ1II+UQ9jS5bfx02H7cIwZTTtuXT0oTEoDN370Z5LQJuCNexYMa+L7 11 | lSjO+FUl+Zji5fE21eObcPE3Z0q1lfTffKWgRD20EJyazAw6v/YM+rUAxShGSltC 12 | 6F+VQhYTea8oOUdKeqHjG8BGVr9pHfJv5De1LP4KDPC6fjyTi3/78GJ2f6D1M2T1 13 | GBz8bBUCgYEA1mw6T0Swc8ZxqWv/a19S2du/WviZ0r97wpLV6vpzNP3mvlnhtknp 14 | rqSUid9E/1OMwq1UuKeBsZnBWtFek7Oml2LyD3T+Pueq7UVyzIxCi+0X6rTLDJwv 15 | 7GEvreSlqCNVSFT1jChsuJbwoAXZSvjKJ0lZToCL/2rj+Ee2k6o/EY8CgYEAwxYd 16 | vv0sB51eiy4E5rD5dub1TvKQtEMLAqrXCnQ5zEsjUMrwLM3RKLDJLxv3ALMEf6fR 17 | RODDAyi0f668bZWqU/rRmZ0uYH0s7MgsbypPnjmkpt6ena1hptGHM6t0FZqhl/fC 18 | XZXSd1CyL6M8mvRMkCsmT9vaxlOSGcZwjNjGjwMCgYEA0sL65aOc1Y//dQqku0Ot 19 | SlsHUkH94Ps6iNyPzDJ1P2c6gWwTwnwQaAt5vbNqPJZBS7HYDtEHWCzLs5sMnZ3+ 20 | Z9toFu1mYyqQBj03Q/uyBZv84ETpOPHk4TH+Da0rb4ObdkecPDIX+7DRwFk/ZsP4 21 | 4HjmgZU1BtBoWEkVGzxCVd0CgYAfgj3xsABhhnhTsG4YOHNCUhFC3AilJMLtflxX 22 | EuiW53ffv7dE9UUX9l24HyMo2MEcQSWao1a79uIYUUebx3WINNWPowRvjygnYjlT 23 | Bxlu6859KS2jN/Kyt9rHbKTGLIB3BAw/g0hH/x+YdBwAxv5qtp1sfoz6RVsukI9z 24 | IDwJswKBgH2IrR72XdD4mMG0U3DgByg759wd9494eIK6d8SPFfNJoz/3NUqZERJy 25 | 224ttnNeztXFL25ezMSH6fcD76Slyh7CHEO/Lzb0fiB80GkTleNI6rlHySr+hqQU 26 | j7oeD13E8ddODFbfPd36hFpZQQH3YWtK8sPP1jfnxaKslYQSIbhE 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /latex/ISR_in_ucore.tex: -------------------------------------------------------------------------------- 1 | \section{【实现】中断处理过程}\label{ux5b9eux73b0ux4e2dux65adux5904ux7406ux8fc7ux7a0b} 2 | 3 | 当中断产生后,首先硬件要完成一系列的工作(如小节``中断处理中硬件负责完成的工作''所描述的``硬件中断处理过程1(起始)''内容),由于中断发生在内核态执行过程中,所以特权级没有变化,所以CPU在跳转到中断处理例程之前,还会在内核栈中依次压入错误码(可选)、EIP、CS和EFLAGS,下图显示了在相同特权级下中断产生后的栈变化示意图: 4 | 5 | \begin{figure}[htbp] 6 | \centering 7 | \includegraphics{figures/3.4.7.1.png} 8 | \caption{3.4.7.1} 9 | \end{figure} 10 | 11 | 然后CPU就跳转到IDT中记录的中断号i所对应的中断服务例程入口地址处继续执行。 12 | vector.S 文件中定义了每个中断的中断处理例程的入口地址 (保存在 vectors 13 | 数组中)。其中,中断可以分成两类:一类是压入错误编码的 (error 14 | code),另一类不压入错误编码。对于第二类, vector.S 自动压入一个 15 | 0。此外,还会压入相应中断的中断号。在内核栈中压入一个或两个必要的参数之后,都会跳转到统一的入口 16 | \_\_alltraps 处(位于trapentry.S中)继续执行。 17 | 18 | CPU从\_\_alltraps处开始,在栈中按照trapframe结构压入各个寄存器,此时内核栈的结构如下所示: 19 | 20 | \begin{lstlisting} 21 | uint32_t reg_edi; 22 | uint32_t reg_esi; 23 | uint32_t reg_ebp; 24 | uint32_t reg_oesp; /* Useless */ 25 | uint32_t reg_ebx; 26 | uint32_t reg_edx; 27 | uint32_t reg_ecx; 28 | uint32_t reg_eax; 29 | uint16_t tf_es; 30 | uint16_t tf_padding1; 31 | uint16_t tf_ds; 32 | uint16_t tf_padding2; 33 | uint32_t tf_trapno; 34 | /* below here defined by x86 hardware */ 35 | uint32_t tf_err; 36 | uintptr_t tf_eip; 37 | uint16_t tf_cs; 38 | uint16_t tf_padding3; 39 | uint32_t tf_eflags; 40 | \end{lstlisting} 41 | 42 | 此时,为了将来能够恢复被打断的内核执行过程所需的寄存器内容都保存好了。为了正确进行中断处理,把DS和ES寄存器设置为GD\_KDATA,这是为了预防从用户态产生的中断(当然,到目前为止,ucore都在内核态执行,还不会发生这种情况)。把刚才保存的trapframe结构的起始地址(即当前SP值)压栈,然后调用 43 | trap函数(定义在trap.c中),就开始了对具体中断的处理。trap进一步调用trap\_dispatch函数,完成对具体中断的处理。在相应的处理过程结束以后,trap将会返回,在\_\_trapret:中,完成对返回前的寄存器和栈的回复准备工作,最后通过iret指令返回到中断打断的地方继续执行。整个中断处理流程大致如下: 44 | 45 | \begin{figure}[htbp] 46 | \centering 47 | \includegraphics{figures/3.4.7.2.png} 48 | \caption{3.4.7.2} 49 | \end{figure} 50 | 51 | \begin{figure}[htbp] 52 | \centering 53 | \includegraphics{figures/3.4.7.3.png} 54 | \caption{3.4.7.3} 55 | \end{figure} 56 | -------------------------------------------------------------------------------- /latex/Makefile: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # Makefile for zhbook 4 | # 5 | # Copyright (C) 2013-2015 Haixing Hu, 6 | # Department of Computer Science and Technology, Nanjing University. 7 | # 8 | # Home Page of the Project: http://haixing-hu.github.io/nju-thesis/ 9 | # 10 | ############################################################################### 11 | 12 | PACKAGE=zhbook 13 | BST_FILE=gbt7714-2005.bst 14 | BST_URL=https://raw.githubusercontent.com/Haixing-Hu/GBT7714-2005-BibTeX-Style/master/gbt7714-2005.bst 15 | SOURCES=$(PACKAGE).dtx $(PACKAGE).ins 16 | CLS=$(PACKAGE).cls $(PACKAGE).cfg dtx-style.sty 17 | #SAMPLE=sample 18 | SAMPLE=sosbook 19 | SAMPLECONTENTS=$(SAMPLE).tex 20 | SAMPLEBIB=$(SAMPLE).bib 21 | TEXMFLOCAL=$(shell get_texmf_dir.sh) 22 | 23 | .PHONY: all clean cls doc sample sosbook 24 | 25 | #all: bst cls doc sample 26 | all: bst cls doc sosbook 27 | 28 | ###### update bst file 29 | bst: $(BST_FILE) 30 | 31 | $(BST_FILE): 32 | curl $(BST_URL) -o $(BST_FILE) 33 | 34 | ###### generate cls/cfg 35 | cls: $(CLS) 36 | 37 | $(CLS): $(SOURCES) 38 | latex $(PACKAGE).ins 39 | 40 | ###### generate doc 41 | 42 | doc: $(PACKAGE).pdf 43 | 44 | $(PACKAGE).pdf: $(CLS) 45 | xelatex $(PACKAGE).dtx 46 | makeindex -s gind.ist -o $(PACKAGE).ind $(PACKAGE).idx 47 | xelatex $(PACKAGE).dtx 48 | xelatex $(PACKAGE).dtx 49 | 50 | ###### for sample 51 | sosbook: $(SAMPLE).pdf 52 | #sample: $(SAMPLE).pdf 53 | 54 | $(SAMPLE).pdf: $(CLS) $(BST_FILE) $(SAMPLE).tex $(SAMPLEBIB) 55 | xelatex $(SAMPLE).tex 56 | bibtex $(SAMPLE) 57 | xelatex $(SAMPLE).tex 58 | xelatex $(SAMPLE).tex 59 | 60 | ###### install 61 | 62 | install: $(SOURCE) $(CLS) $(BST_FILE) $(PACKAGE).pdf $(SAMPLE).pdf 63 | mkdir -p $(TEXMFLOCAL)/tex/latex/zhbook 64 | cp -rvf $(SOURCES) $(CLS) $(TEXMFLOCAL)/tex/latex/zhbook/ 65 | mkdir -p $(TEXMFLOCAL)/doc/latex/zhbook 66 | cp -rvf $(PACKAGE).pdf $(SAMPLE).pdf $(TEXMFLOCAL)/doc/latex/zhbook/ 67 | mkdir -p $(TEXMFLOCAL)/bibtex/bst 68 | cp -rvf $(BST_FILE) $(TEXMFLOCAL)/bibtex/bst/ 69 | texhash 70 | 71 | ###### clean 72 | 73 | clean: 74 | -@rm -f \ 75 | *.aux \ 76 | *.bak \ 77 | *.bbl \ 78 | *.blg \ 79 | *.dvi \ 80 | *.glo \ 81 | *.gls \ 82 | *.idx \ 83 | *.ilg \ 84 | *.ind \ 85 | *.ist \ 86 | *.log \ 87 | *.out \ 88 | *.ps \ 89 | *.thm \ 90 | *.toc \ 91 | *.lof \ 92 | *.lot \ 93 | *.loe \ 94 | *.sty \ 95 | *.cfg \ 96 | *.cls \ 97 | *.sty \ 98 | *.hd \ 99 | *.synctex.gz \ 100 | sample.pdf zhbook.pdf sosbook.pdf 101 | -------------------------------------------------------------------------------- /latex/README.md: -------------------------------------------------------------------------------- 1 | #README 2 | 3 | ## setup 4 | in ubuntu 16.04 x86-64 or later 5 | ``` 6 | ## install texlive 7 | sudo apt install texlive-full 8 | ## install chinese fonts 9 | wget https://github.com/chyyuu/fonts/archive/master.zip 10 | unzip -x fonts-master.zip 11 | wget https://github.com/chyyuu/msfonts/archive/master.zip 12 | unzip -x msfonts-master.zip 13 | sudo mv fonts-master /usr/share/fonts/ 14 | sudo mv msfonts-master /usr/share/fonts/ 15 | sudo apt install fonts-wqy-microhei fonts-wqy-zenhei ttf-wqy-microhei ttf-wqy-zenhei xfonts-wqy 16 | sudo mkfontscale 17 | sudo mkfontdir 18 | sudo fc-cache -fv 19 | ``` 20 | 21 | ## build doc 22 | ``` 23 | make 24 | ``` 25 | 26 | then you will get sosbook.pdf 27 | 28 | -------------------------------------------------------------------------------- /latex/access_harddisk.tex: -------------------------------------------------------------------------------- 1 | \section{【背景】访问硬盘数据控制}\label{ux80ccux666fux8bbfux95eeux786cux76d8ux6570ux636eux63a7ux5236} 2 | 3 | bootloader让80386处理器进入保护模式后,下一步的工作就是从硬盘上加载并运行OS。考虑到实现的简单性,bootloader的访问硬盘都是LBA模式的PIO(Program 4 | IO)方式,即所有的I/O操作是通过CPU访问硬盘的I/O地址寄存器完成。 5 | 6 | 一般主板有2个IDE通道(是硬盘的I/O控制器),每个通道可以接2个IDE硬盘。第一个IDE通道通过访问I/O地址0x1f0-0x1f7来实现,第二个IDE通道通过访问0x170-0x17f实现。每个通道的主从盘的选择通过第6个I/O偏移地址寄存器来设置。具体参数见下表。 7 | 8 | \begin{lstlisting} 9 | I/O地址 功能 10 | 0x1f0 读数据,当0x1f7不为忙状态时,可以读。 11 | 0x1f2 要读写的扇区数,每次读写前,需要指出要读写几个扇区。 12 | 0x1f3 如果是LBA模式,就是LBA参数的0-7位 13 | 0x1f4 如果是LBA模式,就是LBA参数的8-15位 14 | 0x1f5 如果是LBA模式,就是LBA参数的16-23位 15 | 0x1f6 第0~3位:如果是LBA模式就是24-27位 第4位:为0主盘;为1从盘 16 | 第6位:为1=LBA模式;0 = CHS模式 第7位和第5位必须为1 17 | 0x1f7 状态和命令寄存器。操作时先给命令,再读取内容;如果不是忙状态就从0x1f0端口读数据 18 | \end{lstlisting} 19 | 20 | 硬盘数据是储存到硬盘扇区中,一个扇区大小为512字节。读一个扇区的流程大致为通过outb指令访问I/O地址:0x1f2\textasciitilde{}-0x1f7来发出读扇区命令,通过in指令了解硬盘是否空闲且就绪,如果空闲且就绪,则通过inb指令读取硬盘扇区数据都内存中。可进一步参看bootmain.c中的readsect函数实现来了解通过PIO方式访问硬盘扇区的过程。 21 | -------------------------------------------------------------------------------- /latex/appendix.tex: -------------------------------------------------------------------------------- 1 | \chapter{附录}\label{ch_append} 2 | 3 | \input{setuplinuxenv} 4 | \input{setupmammalcomputerenv} -------------------------------------------------------------------------------- /latex/boot.tex: -------------------------------------------------------------------------------- 1 | \chapter{启动操作系统}\label{ch_boot} 2 | 3 | \section{本章概要} 4 | 5 | \paragraph{一句话描述} 6 | 站在操作系统的最底层,了解操作系统的启动,与物理硬件:CPU,内存和多种外设实现“零距离”接触,看到它们并管理它们! 7 | 8 | \paragraph{概述} 9 | 10 | 其实这一章的内容与操作系统原理相关的部分较少,与计算机体系结构的细节相关的部分较多。但这些内容对写一个操作系统关系较大,要知道操作系统是直接与硬件打交道的软件,所以它需要``知道''需要硬件细节,才能更好地控制硬件。另一方面,部分内容涉及到操作系统的重要抽象--中断类异常,能够充分理解中断类异常为以后进一步了解进程切换、上下文切换等概念会很有帮助。 11 | 12 | \paragraph{本章收获的知识} 13 | 14 | \begin{itemize} 15 | \item 16 | 与操作系统原理相关 17 | \item 18 | I/O设备管理:涉及程序循环检测方式和中断启动方式、I/O地址空间 19 | \item 20 | 内存管理:基于分段机制的内存管理 21 | \item 22 | 异常处理:涉及中断、故障和陷阱 23 | \item 24 | 特权级:内核态和用户态 25 | \item 26 | 计算机系统和编程 27 | \item 28 | 硬件 29 | \begin{itemize} 30 | \item 31 | 计算机从加电到加载操作系统内核的整个过程 32 | \item 33 | OS内核在内存中的布局 34 | \item 35 | 串口访问、时钟访问 36 | \end{itemize} 37 | \item 38 | 软件 39 | \begin{itemize} 40 | \item 41 | ELF执行文件格式 42 | \item 43 | 栈的实现并实现函数调用栈跟踪函数 44 | \item 45 | 调试操作系统 46 | \end{itemize} 47 | \end{itemize} 48 | 49 | \paragraph{本章涉及的实验} 50 | 51 | 本章的实验内容涉及的是写一个bootloader能够启动一个操作系统--ucore。在完成bootloader的过程中,逐渐增加bootloader和ucore的能力,涉及CPU的模式切换、解析ELF执行文件格式等,这对于理解操作系统的加载过程以及在操作系统在内存中的位置、内存管理、用户态与内核态的区别等有帮助。而相关project中bootloader和操作系统本身的字符显示的I/O处理、读硬盘数据的I/O处理、键盘/时钟的中断处理等内容,则是操作系统原理中一般在靠后位置提到的设备管理的实际体现。纵观操作系统的发展史,从早期到现在的操作系统主要功能之一就是完成繁琐的I/O处理,给上层应用提供比较简洁的I/O服务,屏蔽硬件处理的复杂性。这也是操作系统的虚拟机功能的体现。另外,本章还介绍了对硬件模拟器的使用,对操作系统的panic处理和远程debug功能的支持,这样有助于读者能够方便地分析操作系统中的错误和调试操作系统。由于本章涉及的硬件知识较多,无疑增大了读者的阅读难度,需要读者在结合阅读本章并实际动手实验来进行深入理解。 52 | 53 | 读者通过阅读本章的内容并动手实践相关的4个实验项目: 54 | 55 | \begin{itemize} 56 | \item 57 | proj1:能够显示字符的bootloader 58 | \item 59 | proj2/3:可读ELF格式文件的bootloader和显示字符的ucore 60 | \item 61 | proj4:可管理中断和处理基于中断的键盘/时钟的ucore 62 | \end{itemize} 63 | 64 | 65 | 66 | 67 | 68 | \input{proj1_small_bootloader} 69 | \input{poweron} 70 | \input{io_access} 71 | \input{protect_mode} 72 | \input{real_mode_switch_protect_mode} 73 | \input{setup_stack} 74 | \input{show_string} 75 | 76 | \input{proj2_bootloader_load_ucore} 77 | \input{access_harddisk} 78 | \input{elf_format} 79 | \input{ucore_code} 80 | \input{load_run_ucore} 81 | \input{show_string_in_ucore} 82 | 83 | \input{proj4_intr_in_ucore} 84 | \input{hardware_intr} 85 | \input{init_intr_controller} 86 | \input{init_IDT} 87 | \input{init_intr_in_device} 88 | \input{ISR_in_ucore} 89 | 90 | \section{小结} 91 | 缺 -------------------------------------------------------------------------------- /latex/deadlock.tex: -------------------------------------------------------------------------------- 1 | % deadlock.tex 2 | \chapter{死锁}\label{ch_deadlock} 3 | 4 | \section{本章概要} 5 | 6 | \paragraph{一句话描述} 7 | 8 | \paragraph{概述} 9 | 10 | \paragraph{本章涉及的实验} 11 | 12 | \paragraph{本章收获的知识} 13 | 14 | %\input{} 15 | %\input{} 16 | %\input{} 17 | %\input{} 18 | %\input{} 19 | %\input{} 20 | %\input{} 21 | 22 | 23 | \section{小结} 24 | 缺 -------------------------------------------------------------------------------- /latex/filesystem.tex: -------------------------------------------------------------------------------- 1 | % filesystem.tex 2 | \chapter{文件系统}\label{ch_filesystem} 3 | 4 | \section{本章概要} 5 | 6 | \paragraph{一句话描述} 7 | 8 | \paragraph{概述} 9 | 10 | \paragraph{本章涉及的实验} 11 | 12 | \paragraph{本章收获的知识} 13 | 14 | %\input{} 15 | %\input{} 16 | %\input{} 17 | %\input{} 18 | %\input{} 19 | %\input{} 20 | %\input{} 21 | 22 | 23 | \section{小结} 24 | 缺 -------------------------------------------------------------------------------- /latex/handle_pages_fault.tex: -------------------------------------------------------------------------------- 1 | \section{【实现】缺页异常处理}\label{ux5b9eux73b0ux7f3aux9875ux5f02ux5e38ux5904ux7406} 2 | 3 | 当启动分页机制以后,如果一条指令或数据的虚拟地址所对应的物理页框不在内存中或者访问的类型有错误(比如写一个只读页或用户态程序访问内核态的数据等),就会发生缺页异常。产生页面异常的原因主要有: 4 | 5 | \begin{itemize} 6 | \item 7 | 目标页面不存在(页表项全为0,即该线性地址与物理地址尚未建立映射或者已经撤销); 8 | \item 9 | 相应的物理页面不在内存中(页表项非空,但Present标志位=0,比如在swap分区或磁盘文件上),这将在下面介绍换页机制实现时进一步讲解如何处理; 10 | \item 11 | 访问权限不符合(此时页表项P标志=1,比如企图写只读页面). 12 | \end{itemize} 13 | 14 | 当出现上面情况之一,那么就会产生页面page 15 | fault(\#PF)异常。产生异常的线性地址存储在CR2中,并且将 \#PF 16 | 的类型保存在 error code 中,比如 bit 0 表示是否 PTE\_P为0,bit 1 17 | 表示是否 write 操作。 18 | 19 | 产生缺页异常后,CPU硬件和软件都会做一些事情来应对此事。首先缺页异常也是一种异常,所以针对一般异常的硬件处理操作是必须要做的,即CPU在当前内核栈保存当前被打断的程序现场,即依次压入当前被打断程序使用的eflags,cs,eip,errorCode;由于缺页异常的中断号是0xE,~CPU把中断0xE服务例程的地址(vectors.S中的标号vector14处)加载到cs和eip寄存器中,开始执行中断服务例程。这时ucore开始处理异常中断,首先需要保存硬件没有保存的寄存器。在vectors.S中的标号vector14处先把中断号压入内核栈,然后再在trapentry.S中的标号\_\_alltraps处把ds、es和其他通用寄存器都压栈。自此,被打断的程序现场被保存在内核栈中。 20 | 21 | 接下来,在trap.c的trap函数开始了中断服务例程的处理流程,大致调用关系为: 22 | 23 | \begin{lstlisting} 24 | trap--> trap_dispatch-->pgfault_handler-->do_pgfault 25 | \end{lstlisting} 26 | 27 | 下面需要具体分析一下do\_pgfault函数。CPU把引起缺页异常的虚拟地址装到寄存器CR2中,并给出了出错码(tf-\textgreater{}tf\_err),指示引起缺页异常的存储器访问的类型。而中断服务例程会调用缺页异常处理函数do\_pgfault进行具体处理。缺页异常处理是实现按需分页、swap 28 | in/out和写时复制的关键之处,后面的小节将分别展开讲述。 29 | 30 | ucore中do\_pgfault函数是完成缺页异常处理的主要函数,它根据从CPU的控制寄存器CR2中获取的缺页异常的虚拟地址以及根据 31 | error 32 | code的错误类型来查找此虚拟地址是否在某个VMA的地址范围内以及是否满足正确的读写权限,如果在此范围内并且权限也正确,这认为这是一次合法访问,但没有建立虚实对应关系。所以需要分配一个空闲的内存页,并修改页表完成虚地址到物理地址的映射,刷新TLB,然后调用iret中断,返回到产生缺页异常的指令处重新执行此指令。如果该虚地址不再某VMA范围内,这认为是一次非法访问。 33 | 34 | \textbf{【注意】} 35 | 36 | 地址空间的管理由虚存管理和页表管理两部分组成。 37 | 虚存管理限制了(程序)地址空间的范围以及权限,而页表维护的是实际使用的地址空间以及权限,后者不能比前者有更大的范围或者权限,因为前者是实际管理页表的。比如权限,虚存管理可以规定地址空间的某个范围是可写的,但是页表中却可以标记是read-only的(比如 38 | copy-on-write 39 | 的实现),这种冲突可以被内核(通过硬件异常)轻易的捕获到,并进行相应的处理。反过来,如果页表权限比虚存规定的权限更大,内核是没有办法发现这种冲突的。由于虚存管理的存在,内核才能方便的实现更复杂和丰富的操作,比如 40 | share memory、swap 等。 41 | 在后续的实验中还会遇到虚存管理只维护用户地址空间(也就是 {[}USERBASE, 42 | USERTOP) 区间)的情况,因为内核地址空间包括虚存和页表都是固定的。 43 | -------------------------------------------------------------------------------- /latex/hardware.tex: -------------------------------------------------------------------------------- 1 | \section{了解计算机硬件架构} 2 | 3 | \subsection{一般计算机硬件架构} 4 | 5 | 这里介绍一下运行操作系统的基本计算机硬件架构。一台计算机可抽象一台以图灵机(Turing Machine)为理想模型,以冯诺依曼架构( Von Neumann Architecture)为实现模型的电子设备,包括CPU、memory和 I/O 设备。CPU(中央处理器,也称处理器) 执行操作系统中的指令,完成相关计算和读写内存,物理内存保存了操作系统中的指令和需要处理的数据,外部设备用于实现操作系统的输入(键盘、硬盘),输出(显示器、并口、串口),计时(时钟)永久存储(硬盘)。操作系统除了能在计算机上运行外,还要管好计算机。下面将简要介绍计算机硬件以及操作系统大致要完成的事情。 6 | 7 | 8 | 9 | \subsubsection{CPU} 10 | 11 | CPU是计算机系统的核心,目前存在各种8位,16位,32位,64位的CPU,应用在从嵌入式系统到巨型机等不同的场合中。CPU从一加电开始,从某设定好的内存地址开始,取指令,执行指令,并周而复始地运行。取指令的过程即从某寄存器(比如,程序计数器)中获取一个内存地址,从这个内存地址中读入指令,执行机器指令,不断重复,CPU运行期间会有分支和调用指令来修改程序计数器,实现地址跳转,否则程序计数器就自动加1,让CPU从下一个内存地址单元取指令,并继续执行。 12 | 13 | 由于CPU执行速度很快(x86 CPU可达到2GHZ以上的时钟频率,RISC-V CPU可达到1.5 GHZ的时钟频率),如果当前可以运行的程序太少,则会出现CPU无事可做的情况,导致计算机系统效率太低。这时就需要操作系统来帮忙了,我们需要操作系统除了能管理硬件外,还能管理应用程序,让它们能够按一定的顺序和优先级来依次使用CPU,充分发挥CPU的效能。操作系统管一个程序比较容易,但如果管理多个程序的运行,就需要考虑如何分配CPU资源的问题,如何避免程序执行期间发生“冲突”的问题等,这是操作系统需要完成的重要功能之一。 14 | 15 | \subsubsection{memory} 16 | 17 | 计算机中有多种多层次的存放数据和指令代码的硬件存储单元,比如在CPU内的寄存器(register)、高速缓存(cache)、内存(memory)、硬盘、磁带等。寄存器位于CPU内部,其访问速度最快但成本昂贵,在对于传统的CISC(复杂指令集计算机,如Intel 80386处理器)中一般只有几个到十个左右的通用寄存器,而对于RISC(精简指令集计算机,如RISC-V),则可能有几十个以上通用寄存器;高速缓存(cache) 一般也在CPU内部,cache是内存和寄存器在速度和大小上的折衷,比寄存器慢2~10倍,容量也有限,量级大约几百KB到几十MB不等;再接下来就是内存了,内存位于CPU外,比寄存器慢10倍以上,但容量大,目前一般以几百兆B到几百GB不等;硬盘容量更大,但一般比寄存器要慢1000倍以上,不过掉电后其存储的数据不会丢失。 18 | 19 | 由于寄存器、cache、内存、硬盘在读写速度和容量上的巨大差异,所以需要操作系统来协调数据的访问,尽量主动协助应用软件,把最近访问的数据放到寄存器或cache中(实际上操作系统不能直接控制cache的读写),把经常访问的数据放在内存中,把不常用的数据放到硬盘上,这样可以达到让多个运行的应用程序“感觉”到它可用使用很大的空间,也可有很快的访问速度。如何让在运行中的每个程序都能够得到“足够大”的内存空间,且程序间相互不能破坏对方的内存“领地”,且建立他们之间的“数据共享”通道,这是操作系统需要完成的重要功能之一。 20 | 21 | \subsubsection{I/O} 22 | 23 | 24 | CPU处理的数据需要有来源和输出,这里的来源和输出就是各种外设,如大家日常使用计算机用到的键盘、鼠标、显示器、声卡、GPU、U盘、硬盘、SSD存储、打印机、网卡、摄像头等。上图中给出了PC计算机的一些外设硬件。应用程序如果直接访问外设,会有代码实现复杂,可移植性差,无法有效并发等问题,所以操作系统给应用程序提供了简单的访问接口,让应用程序不需要了解硬件细节。具体访问外设的苦活累活都交给操作系统来完成了,这就是操作系统中外设驱动哦程序的主要功能。 25 | 26 | 如果操作系统要通过CPU对数据进行加工,首先需要有输入,在处理完后还要进行输出,否则没东西要处理或者执行完了无法把结果反馈给用户。操作系统要处理的数据需要从外设(比如键盘、串口、硬盘、网卡)中获得,这就是一种读外设数据的操作;且在处理完毕后要传给外设(比如显示器、硬盘、打印机、网卡)进一步处理,这其实就是一种写外设数据的操作。 27 | 28 | 一般而言,IO外设把它的访问接口映射为一块内存区域,操作系统通过来用通常的内存读写指令来管理设备;或者CPU提供了特定的IO操作指令,操作系统通过这些特定的指令来完成对IO外设的访问。并且操作系统可以通过轮循、中断、DMA等访问方式来高效地管理外设。 29 | 30 | 比如,在RISC-V中通过对特定地址的内存(代表IO外设的接口)进行读或写访问,就可以实现对IO外设的访问了。在Intel x86中有两条特殊的 in和out 指令来在完成CPU对外设地址空间的访问,实现对外设的管理控制。本书不会涉及很多复杂具体硬件,而只涉及到操作系统用到的一些最基本的外设硬件(时钟,串口,硬盘)细节。 31 | 32 | 33 | \input{riscv} 34 | -------------------------------------------------------------------------------- /latex/implement_copy_on_write.tex: -------------------------------------------------------------------------------- 1 | \section{proj9.2:实现写时复制}\label{proj9.2ux5b9eux73b0ux5199ux65f6ux590dux5236} 2 | 3 | proj9.2实现了写时复制(Copy On 4 | Write,简称COW)的主要功能,为lab3高效地创建子进程打下了基础。COW有何作用?这里又不得不提前讲讲lab3中的子进程创建。不同的进程应该具有不同的物理内存空间,当用户态进程发出fork( 5 | )系统调用来创建子进程时,ucore可复制当前进程(父进程)的整个地址空间,这样就有两块不同的物理地址空间了,新复制的那一块物理地址空间分配给子进程。这种行为是非常耗时和占内存资源的,因为它需要为子进程的页表分配页面,复制父进程的每一个物理内存页。如果子进程加载一个新的程序开始执行(这个过程会释放掉原来申请的全部内存和资源),这样前面的复制工作就白做了,完全没有必要。 6 | 7 | 为了解决上述问题,ucore采用一种有效的COW机制。其设计思想相对简单:父进程和子进程之间共享(share)页面而不是复制(copy)页面。但只要页面被共享,它们就不能被修改,即是只读的。注意此共享是指父子进程共享一个表示内存空间的mm\_struct结构的变量。当父进程或子进程试图写一个共享的页面,就产生一个页访问异常,这时内核就把这个页复制到一个新的页面中并标记为可写。注意,原来的页面仍然是写保护的。当其它进程试图写入时,ucore检查写进程是否是这个页面的唯一属主(通过判断page\_ref 8 | 和 swap\_page\_count 即 mem\_map 中相关 entry 9 | 保存的值的和是否为1。注意区分与share memory的差别,share memory 通过 vma 10 | 中的 shmem 实现,这样的 page 是直接标记为共享的,而不是 copy on 11 | write,所以也没有任何冲突);如果是,它把这个页面标记为对这个进程是可写的。 12 | 13 | 在具体实现上,ucore调用dup\_mmap函数,并进一步调用copy\_range函数来具体完成对页表内容的复制,这样两个页表表示同一个虚拟地址空间(包括对应的物理地址空间),且还需修改两个页表中每一个页对应的页表项属性为只读,但。在这种情况下,两个进程有两个页表,但这两个页表只映射了一块只读的物理内存。同理,对于换出的页,也采用同样的办法来共享一个换出页。综上所述,我们可以总结出:如果一个页的PTE属性是只读的,但此页所属的VMA描述指出其虚地址空间是可写的,则这样的页是COW页。 14 | 15 | 当对这样的地址空间进行写操作的时候,会触发do\_pgfault函数被调用。此函数如果发现是COW页,就会调用alloc\_page函数新分配一个物理页,并调用memcpy函数把旧页的内容复制到新页中,并最后调用page\_insert函数给当前产生缺页错的进程建立虚拟页地址到新物理页地址的映射关系(即改写PTE,并设置此页为可读写)。 16 | 17 | 这里还有一个特殊情况,如果产生访问异常的页已经被换出到硬盘上了,则需要把此页通过swap\_in\_page函数换入到内存中来,如果进一步发现换入的页是一个COW页,则把其属性设置为只读,然后异常处理结束返回。但这样重新执行产生异常的写操作,又会触发一次内存访问异常,则又要执行上一段描述的过程了。 18 | 19 | Page结构的ref域用于跟踪共享相应页面的进程数目。只要进程释放一个页面或者在它上面执行写时复制,它的ref域就递减;只有当ref变为0时,这个页面才被释放。 20 | -------------------------------------------------------------------------------- /latex/init_IDT.tex: -------------------------------------------------------------------------------- 1 | \section{【实现】初始化中断门描述符表}\label{ux5b9eux73b0ux521dux59cbux5316ux4e2dux65adux95e8ux63cfux8ff0ux7b26ux8868} 2 | 3 | ucore操作系统如果要正确处理各种不同的中断事件,就需要安排应该由哪个中断服务例程负责处理特定的中断事件。系统将所有的中断事件统一进行了编号(0~255),这个编号称为中断号或中断向量。 4 | 5 | 为了完成中断号和中断服务例程起始地址的对应关系,首先需要建立256个中断处理例程的入口地址。为此,通过一个 6 | C程序 tools/vector.c 生成了一个文件vectors.S,在此文件中的 7 | \_\_vectors地址处开始处连续存储了256个中断处理例程的入口地址数组,且在此文件中的每个中断处理例程的入口地址处,实现了中断处理过程的第一步初步处理。 8 | 9 | 有了中断服务例程的起始地址,就可以建立对应关系了,这部分的实现在trap.c文件中的idt\_init函数中实现: 10 | 11 | \begin{lstlisting} 12 | //全局变量:中断门描述符表 13 | 14 | static struct gatedesc idt[256] = {{0}}; 15 | …… 16 | void idt_init(void) { 17 | 18 | //保存在vectors.S中的256个中断处理例程的入口地址数组 19 | 20 | extern uint32_t __vectors[]; 21 | int i; 22 | 23 | //在中断门描述符表中通过建立中断门描述符,其中存储了中断处理例程的代码段GD_KTEXT和偏移量\__vectors[i],特权级为DPL_KERNEL。这样通过查询idt[i]就可定位到中断服务例程的起始地址。 24 | 25 | for (i = 0; i < sizeof(idt) / sizeof(struct gatedesc); i ++) { 26 | SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL); 27 | } 28 | 29 | //建立好中断门描述符表后,通过指令lidt把中断门描述符表的起始地址装入IDTR寄存器中,从而完成中段描述符表的初始化工作。 30 | 31 | lidt(&idt_pd); 32 | } 33 | \end{lstlisting} 34 | 35 | -------------------------------------------------------------------------------- /latex/init_intr_controller.tex: -------------------------------------------------------------------------------- 1 | \section{【实现】初始化中断控制器}\label{ux5b9eux73b0ux521dux59cbux5316ux4e2dux65adux63a7ux5236ux5668} 2 | 3 | 80386把中断号0~31分配给陷阱、故障和非屏蔽中断,而把32~47之间的中断号分配给可屏蔽中断。可屏蔽中断的中断号是通过对中断控制器的编程来设置的。下面描述了对8259A中断控制器初始化过程。 4 | 5 | 8259A通过两个I/O地址来进行中断相关的数据传送,对于单个的8259A或者是两级级联中的主8259A而言,这两个I/O地址是0x20和0x21。对于两级级联的从8259A而言,这两个I/O地址是0xA0和0xA1。8259A有两种编程方式,一是初始化方式,二是工作方式。在操作系统启动时,需要对8959A做一些初始化工作,即实现8259A的初始化方式编程。8259A中的四个中断命令字(ICW)寄存器用来完成初始化编程,其含义如下: 6 | 7 | \begin{itemize} 8 | \item 9 | ICW1:初始化命令字。 10 | \item 11 | ICW2:中断向量寄存器,初始化时写入高五位作为中断向量的高五位,然后在中断响应时由8259根据中断源(哪个管脚)自动填入形成完整的8位中断向量(或叫中断类型号)。 12 | \item 13 | ICW3: 8259的级联命令字,用来区分主片和从片。 14 | \item 15 | ICW4:指定中断嵌套方式、数据缓冲选择、中断结束方式和CPU类型。 16 | \end{itemize} 17 | 18 | 8259A初始化的过程就是写入相关的命令字,8259A内部储存这些命令字,以控制8259A工作。有关的硬件可看附录补充资料。这里只把ucore对8259A的初始化过程(在picirq.c中的pic\_init函数实现)描述一下: 19 | 20 | \begin{lstlisting} 21 | //此时系统尚未初始化完毕,故屏蔽主从8259A的所有中断 22 | 23 | outb(IO_PIC1 + 1, 0xFF); 24 | outb(IO_PIC2 + 1, 0xFF); 25 | 26 | // 设置主8259A的ICW1,给ICW1写入0x11,0x11表示(1)外部中断请求信号为上升沿触发有效,(2)系统中有多片8295A级联,(3)还表示要向ICW4送数据 27 | 28 | // ICW1设置格式为: 0001g0hi 29 | // g: 0 = edge triggering, 1 = level triggering 30 | // h: 0 = cascaded PICs, 1 = master only 31 | // i: 0 = no ICW4, 1 = ICW4 required 32 | 33 | outb(IO_PIC1, 0x11); 34 | 35 | // 设置主8259A的ICW2: 给ICW2写入0x20,设置中断向量偏移值为0x20,即把主8259A的IRQ0-7映射到向量0x20-0x27 36 | 37 | outb(IO_PIC1 + 1, IRQ_OFFSET); 38 | 39 | // 设置主8259A的ICW3: ICW3是8259A的级联命令字,给ICW3写入0x4,0x4表示此主中断控制器的第2个IR线(从0开始计数)连接从中断控制器。 40 | 41 | outb(IO_PIC1 + 1, 1 << IRQ_SLAVE); 42 | 43 | //设置主8259A的ICW4:给ICW4写入0x3,0x3表示采用自动EOI方式,即在中断响应时,在8259A送出中断矢量后,自动将ISR相应位复位;并且采用一般嵌套方式,即当某个中断正在服务时,本级中断及更低级的中断都被屏蔽,只有更高的中断才能响应。 44 | 45 | // ICW4设置格式为: 000nbmap 46 | // n: 1 = special fully nested mode 47 | // b: 1 = buffered mode 48 | // m: 0 = slave PIC, 1 = master PIC 49 | // (ignored when b is 0, as the master/slave role 50 | // can be hardwired). 51 | // a: 1 = Automatic EOI mode 52 | // p: 0 = MCS-80/85 mode, 1 = intel x86 mode 53 | outb(IO_PIC1 + 1, 0x3); 54 | 55 | //设置从8259A的ICW1:含义同上 56 | 57 | outb(IO_PIC2, 0x11); // ICW1 58 | 59 | //设置从8259A的ICW2:给ICW2写入0x28,设置从8259A的中断向量偏移值为0x28 60 | 61 | outb(IO_PIC2 + 1, IRQ_OFFSET + 8); // ICW2 62 | 63 | //0x2表示此从中断控制器链接主中断控制器的第2个IR线 64 | 65 | outb(IO_PIC2 + 1, IRQ_SLAVE); // ICW3 66 | 67 | //设置主8259A的ICW4:含义同上 68 | 69 | outb(IO_PIC2 + 1, 0x3); // ICW4 70 | 71 | //设置主从8259A的OCW3:即设置特定屏蔽位(值和英文解释不一致),允许中断嵌套;不查询;将读入其中断请求寄存器IRR的内容 72 | 73 | // OCW3设置格式为: 0ef01prs 74 | // ef: 0x = NOP, 10 = clear specific mask, 11 = set specific mask 75 | // p: 0 = no polling, 1 = polling mode 76 | // rs: 0x = NOP, 10 = read IRR, 11 = read ISR 77 | outb(IO_PIC1, 0x68); // clear specific mask 78 | outb(IO_PIC1, 0x0a); // read IRR by default 79 | 80 | outb(IO_PIC2, 0x68); // OCW3 81 | outb(IO_PIC2, 0x0a); // OCW3 82 | 83 | //初始化完毕,使能主从8259A的所有中断 84 | 85 | if (irq_mask != 0xFFFF) { 86 | pic_setmask(irq_mask); 87 | } 88 | \end{lstlisting} 89 | 90 | -------------------------------------------------------------------------------- /latex/init_intr_in_device.tex: -------------------------------------------------------------------------------- 1 | \section{【实现】外设的相关中断初始化}\label{ux5b9eux73b0ux5916ux8bbeux7684ux76f8ux5173ux4e2dux65adux521dux59cbux5316} 2 | 3 | 串口的初始化函数serial\_init(位于/kern/driver/console.c)中涉及中断初始化工作的很简单: 4 | 5 | \begin{lstlisting} 6 | ...... 7 | // 使能串口1接收字符后产生中断 8 | outb(COM1 + COM_IER, COM_IER_RDI); 9 | ...... 10 | // 通过中断控制器使能串口1中断 11 | pic_enable(IRQ_COM1); 12 | \end{lstlisting} 13 | 14 | 键盘的初始化函数kbd\_init(位于kern/driver/console.c中)完成了对键盘的中断初始化工作,具体操作更加简单: 15 | 16 | \begin{lstlisting} 17 | ...... 18 | // 通过中断控制器使能键盘输入中断 19 | pic_enable(IRQ_KBD); 20 | \end{lstlisting} 21 | 22 | 时钟是一种有着特殊作用的外设,其作用并不仅仅是计时。在后续章节中将讲到,正是由于有了规律的时钟中断,才使得无论当前CPU运行在哪里,操作系统都可以在预先确定的时间点上获得CPU控制权。这样当一个应用程序运行了一定时间后,操作系统会通过时钟中断获得CPU控制权,并可把CPU资源让给更需要CPU的其他应用程序。时钟的初始化函数clock\_init(位于kern/driver/clock.c中)完成了对时钟控制器8253的初始化: 23 | 24 | \begin{lstlisting} 25 | ...... 26 | //设置时钟每秒中断100次 27 | outb(IO_TIMER1, TIMER_DIV(100) % 256); 28 | outb(IO_TIMER1, TIMER_DIV(100) / 256); 29 | // 通过中断控制器使能时钟中断 30 | pic_enable(IRQ_TIMER); 31 | \end{lstlisting} 32 | 33 | -------------------------------------------------------------------------------- /latex/intro.tex: -------------------------------------------------------------------------------- 1 | 2 | \chapter{绪论}\label{ch_intro} 3 | 4 | \section{本章概要} 5 | 6 | \paragraph{一句话描述} 7 | 8 | 用”hello world“程序作为向导,站在一万米的高空看操作系统的发展和特征! 9 | 10 | \paragraph{概述} 11 | 12 | 计算机系统是由硬件和系统软件((主要是操作系统)组成的,它们共同工作来运行应用程序。纵观计算机相对很短的发展史,虽然计算机系统的具体实现方式不断变化,越来越复杂,但是计算机系统内在的概念保持相对稳定。所有计算机系统在原理上基于天才的图灵机,在设计上基于伟大的冯诺依曼架构,有相似的硬件和软件组件,它们执行着相似的功能。本章主要以操作系统为主来理解计算机系统如何支持一个C语言编写的”hello world“程序完成它的功能。 13 | 14 | 这里先简要介绍一下操作系统的历史,然后将从一个C语言编写的”hello world“程序开始,讲述操作系统和涉及到的编译,计算机原理等的一些基础知识,以及对操作系统的基本架构和用于本书的ucore教学操作系统做一个初步介绍。在其中穿插介绍操作系统的基本概念、操作系统抽象以及操作系统的特征。 15 | 16 | % 17 | %本书希望通过设计实现操作系统来更好地理解操作系统原理和概念。设计实现操作系统其实就是设计实现一个可以管理CPU、内存和各种外设,并管理和服务应用软件的系统软件。为此还是需要先了解一些基本的计算机原理和编程的知识。本书的例子和描述需要读者学习过计算机原理课程、程序设计课程,掌握C语言编程(了解指针等的编程)。如需完成基于RISCore实验,则对基于RISC-V的体系结构有一定的了解,大致了解RISC-V的汇编语言。 18 | 19 | %\input{prerequisite} 20 | \input{overlook} 21 | \input{hardware} 22 | \input{ucore} 23 | 24 | \section{小结} 25 | 本章以分析一个“hello world”程序的执行过程为例子,概要地介绍了操作系统运行的计算机硬件架构,包括CPU、内存和外设,并对操作系统的历史发展、定义、目标、接口、抽象和特征等进行了阐述。最后简要介绍了课程实验用到的ucore操作系统。 -------------------------------------------------------------------------------- /latex/ipc.tex: -------------------------------------------------------------------------------- 1 | % ipc.tex 2 | \chapter{进程间通信}\label{ch_ipc} 3 | 4 | \section{本章概要} 5 | 6 | \paragraph{一句话描述} 7 | 8 | \paragraph{概述} 9 | 10 | \paragraph{本章涉及的实验} 11 | 12 | \paragraph{本章收获的知识} 13 | 14 | %\input{} 15 | %\input{} 16 | %\input{} 17 | %\input{} 18 | %\input{} 19 | %\input{} 20 | %\input{} 21 | 22 | 23 | \section{小结} 24 | 缺 -------------------------------------------------------------------------------- /latex/kernel_to_user.tex: -------------------------------------------------------------------------------- 1 | \section{【实现】内核态切换到用户态}\label{ux5b9eux73b0ux5185ux6838ux6001ux5207ux6362ux5230ux7528ux6237ux6001} 2 | 3 | 在kern/init.c中的switch\_test函数完成了内核态\textless{}--\textgreater{}用户态之间的切换。内核态切换到用户态是通过swtich\_to\_user函数,执行指令``int 4 | T\_SWITCH\_TOU''。当CPU执行这个指令时,由于是在switch\_to\_user执行在内核态,所以不存在特权级切换问题,硬件只会在内核栈中压入Error 5 | Code(可选)、EIP、CS和EFLAGS(如下图所示),然后跳转到到IDT中记录的中断号T\_SWITCH\_TOU所对应的中断服务例程入口地址处继续执行。通过2.3.7小节``中断处理过程''可知,会执行到trap\_disptach函数(位于trap.c): 6 | 7 | \begin{lstlisting} 8 | case T_SWITCH_TOU: 9 | if (tf->tf_cs != USER_CS) { 10 | //当前在内核态,需要建立切换到用户态所需的trapframe结构的数据switchk2u 11 | switchk2u = *tf; 12 | switchk2u.tf_cs = USER_CS; 13 | switchk2u.tf_ds = switchk2u.tf_es = switchk2u.tf_ss = USER_DS; 14 | switchk2u.tf_esp = (uint32_t)tf + sizeof(struct trapframe) - 8; 15 | //设置EFLAG的I/O特权位,使得在用户态可使用in/out指令 16 | switchk2u.tf_eflags |= (3 << 12); 17 | //设置临时栈,指向switchk2u,这样iret返回时,CPU会从switchk2u恢复数据, 18 | //而不是从现有栈恢复数据。 19 | *((uint32_t *)tf - 1) = (uint32_t)&switchk2u; 20 | } 21 | \end{lstlisting} 22 | 23 | 这样在trap将会返回,在\_\_trapret:中,根据switchk2u的内容完成对返回前的寄存器和栈的回复准备工作,最后通过iret指令,CPU返回``int 24 | T\_SWITCH\_TOU''的后一条指令处,以用户态模式继续执行。 25 | 26 | \begin{figure}[htbp] 27 | \centering 28 | \includegraphics{figures/3.5.5.1.png} 29 | \caption{3.5.5.1} 30 | \end{figure} 31 | -------------------------------------------------------------------------------- /latex/os_abstract.tex: -------------------------------------------------------------------------------- 1 | \subsubsection{操作系统抽象} 2 | 3 | 接下来读者可站在操作系统实现的角度来看操作系统。操作系统为了能够更好地管理计算机系统并对应用程序提供便捷的服务,在操作系统的发展过程中,计算机科学家提出了如下四个个抽象概念,奠定了操作系统内核设计与实现的基础。操作系统原理中的其他基本概念基本上都基于上述这四个操作系统抽象。 4 | 5 | \paragraph{中断(Interrupt)} 6 | 7 | 简单地说,中断是处理器在执行过程中的突变,用来响应处理器状态中的特殊变化。比如当应用程序正在执行时,产生了时钟外设中断,导致操作系统打断当前应用程序的执行,转而去处理时钟外设中断,处理完毕后,再回到应用程序被打断的地方继续执行。在操作系统中,有三类中断:外设中断(Device Interrupt)、陷阱中断(Trap Interrupt)和故障中断(Fault Interrupt,也称为exception,异常)。外设中断由外部设备引起的外部I/O事件如时钟中断、控制台中断等。外设中断是异步产生的,与处理器的执行无关。故障中断是在处理器执行指令期间检测到不正常的或非法的内部事件(如除零错、地址访问越界)。陷阱中断是在程序中使用请求操作系统服务的系统调用而引发的有意事件。在后面的叙述中,如果没有特别指出,我们将用简称中断、陷阱、故障来区分这三种特殊的中断事件,在不需要区分的地方,统一用中断表示。 8 | 9 | \paragraph{进程(Process)} 10 | 11 | 简单地说,进程是一个正在运行的程序。在计算机系统中,我们可以“同时”运行多个程序,这个“同时”,其实是操作系统给用户造成的一个“幻觉”。大家知道,处理器是计算机系统中的硬件资源。为了提高处理器的利用率,操作系统采用了多道程序技术。如果一个程序因某个事件而不能运行下去时,就把处理器占用权转交给另一个可运行程序。为了刻画多道程序的并发执行的过程,就要引入进程的概念。从操作系统原理上看,一个进程是一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程。操作系统中的进程管理需要协调多道程序之间的关系,解决对处理器分配调度策略、分配实施和回收等问题,从而使得处理器资源得到最充分的利用。 12 | 13 | \paragraph{虚存(Virtual Memory)} 14 | 15 | 简单地说,虚存就是操作系统通过处理器中的MMU硬件的支持而给应用程序和用户提供一个大的(超过计算机中的内存条容量)、一致的(连续的地址空间)、私有的(其他应用程序无法破坏)的存储空间。这需要操作系统将内存和硬盘结合起来管理,为用户提供一个容量比实际内存大得多的虚拟存储器,并且需要操作系统为应用程序分配内存空间,使用户存放在内存中的程序和数据彼此隔离、互不侵扰。操作系统中的虚存管理与处理器的MMU密切相关。 16 | 17 | \paragraph{文件(File)} 18 | 19 | 简单地说,文件就是存放在持久存储介质(比如硬盘、光盘、U盘等)上,方便应用程序和用户读写的数据。当处理器需要访问文件中的数据时,可通过操作系统把它们装入内存。放在硬盘上的程序也是一种文件。文件管理的任务是有效地支持文件的存储、检索和修改等操作。 20 | -------------------------------------------------------------------------------- /latex/os_define.tex: -------------------------------------------------------------------------------- 1 | \subsection{操作系统的定义与目标} 2 | 3 | 4 | \paragraph{操作系统的定义} 5 | 6 | 有了对硬件的进一步了解,我们就可以给操作系统下一个更准确一些的定义。操作系统是计算机系统机构中的一个系统软件,它的职能主要有两个:对下面(也就是计算机硬件),有效地组织和管理计算机系统中的硬件资源(包括处理器、内存、硬盘、显示器、键盘、鼠标等各种外设);对上面(应用程序或用户),提供简洁的服务功能接口,屏蔽硬件管理带来的差异性和复杂性,使得应用程序和用户能够灵活、方便、有效地使用计算机。为了完成这两个职能,操作系统需要起到资源管理器的作用,能在其内部实现中安全,合理地组织,分配,使用与处理计算机中的软硬件资源,使整个计算机系统能高效可靠地运行。 7 | 8 | \paragraph{操作系统的目标} 9 | 10 | 根据前面的介绍,我们可以看出操作系统有如下一些目标: 11 | 12 | \begin{enumerate} 13 | \item 建立抽象,让上层软件和用户更方便使用; 14 | \item 管理软硬件资源,确保计算机系统安全可靠、高性能; 15 | \item 其他需求:节能、易用、可移植、实时等等。 16 | \end{enumerate} 17 | 18 | -------------------------------------------------------------------------------- /latex/os_feature.tex: -------------------------------------------------------------------------------- 1 | \subsubsection{操作系统特征} 2 | 3 | 基于操作系统的四个抽象,我们可以看出,从总体上看,操作系统具有五个方面的特征:虚拟性(Virtualization)、并发性(concurrency)、异步性、共享性和持久性(persistency)。在虚拟性方面,可以从操作系统对内存,CPU的抽象和处理上有更好的理解;对于并发性和共享性方面,可以从操作系统支持多个应用程序“同时”运行的情况来理解;对于异步性,可以从操作系统调度,中断处理对应用程序执行造成的影响等几个放马来理解;对于持久性方面,可以从操作系统中的文件系统支持把数据方便地从磁盘等存储介质上存入和取出来理解。 4 | 5 | \paragraph{虚拟性} 6 | 7 | \subparagraph{内存虚拟化} 8 | 9 | 首先来看看内存的虚拟化。程序员在写应用程序的时候,不用考虑其程序的起始内存地址要放到计算机内存的具体某个位置,而是用字符串符号定义了各种变量和函数,直接在代码中便捷地使用这些符号就行了。这是由于操作系统建立了一个地址固定,空间巨大的虚拟内存给应用程序来运行,这是空间虚拟化。这里的每个符号在运行时是要对应到具体的内存地址的。这些内存地址的具体数值是什么?程序员不用关心。为什么?因为编译器会自动帮我们吧这些符号翻译成地址,形成可执行程序。程序使用的内存是否占得太大了?在一般情况下,程序员也不用关心。 10 | 11 | 还记得虚拟地址(逻辑地址)的描述吗? 12 | 13 | 但编译器(compiler,比如gcc)和链接器(linker,比如ld)也不知道程序每个符号对应的地址应该放在未来程序运行时的哪个物理内存地址中。所以,编译器的一个简单处理办法就是,设定一个固定地址(比如 0x10000)作为起始地址,开始存放代码,代码之后是数据,所有变量和函数的符号都在这个起始地址之后的某个固定偏移位置。假定程序每次运行都是位于一个不会变化的起始地址。 14 | 15 | 这里的变量指的是全局变量,其地址在编译链接后会确定不变。但局部变量是放在堆栈中的,会随着堆栈大小的动态变化而变化。 16 | 17 | 这里编译器产生的地址就是虚拟地址。 18 | 19 | 这里,编译器和链接器图省事,找了一个适合它们的解决办法。当程序要运行的时候,这个符号到机器物理内存的映射必须要解决了,这自然就推到了操作系统身上。操作系统会把编译器和链接器生成的执行代码和数据放到物理内存中的空闲区域中,并建立虚拟地址到物理地址的映射关系。由于物理内存中的空闲区域是动态变化的,这也导致虚拟地址到物理地址的映射关系是动态变化的,需要操作系统来维护好可变的映射关系,确保编译器“固定起始地址”的假设成立。只有操作系统维护好了这个映射关系,才能让程序员只需写一些易于人理解的字符串符号来代表一个内存空间地址,且编译器只需确定一个固定地址作为程序的起始地址就可以生成一个不用考虑将来这个程序要在哪里运行的问题,从而实现了空间虚拟化。 20 | 21 | 应用程序在运行时不用考虑当前物理内存是否够用。如果应用程序需要一定空间的内存,但由于在某些情况下,物理内存的空闲空间可能不多了,这时操作系统通过把物理内存中最近没使用的空间(不是空闲的,只是最近用得少)换出(就是“挪地”)到硬盘上暂时缓存起来,这样空闲空间就大了,就可以满足应用程序的运行时内存需求了,从而实现了空间大小虚拟化。 22 | 23 | \subparagraph{CPU虚拟化} 24 | 25 | 再来看CPU虚拟化。不同的应用程序可以在内存中并发运行,相同的应用程序也可有多个拷贝在内存中并发运行。而每个程序都“认为”自己完全独占了CPU在运行,这是”时间虚拟化“。这其实也是操作系统给了运行的应用程序一个虚拟幻象。其实是操作系统把时间分成小段,每个应用程序占用其中一小段时间片运行,用完这一时间片后,操作系统会切换到另外一个应用程序,让它运行。由于时间片很短,操作系统的切换开销也很小,人眼基本上是看不出的,反而感觉到多个程序各自在独立”并行“执行,从而实现了时间虚拟化。 26 | 27 | 并行(Parallel)是指两个或者多个事件在同一时刻发生;而并发(Concurrent)是指两个或多个事件在同一时间间隔内发生。 28 | 29 | 对于单CPU的计算机而言,各个”同时“运行的程序其实是串行分时复用一个CPU,任一个时刻点上只有一个程序在CPU上运行。 30 | 31 | 这些虚拟性的特征给应用程序的开发和执行提供了非常方便的环境,但也给操作系统的设计与实现提出了很多挑战。 32 | 33 | \paragraph{并发性} 34 | 35 | 操作系统为了能够让CPU充分地忙起来并充分利用各种资源,就需要给很多任务给它去完成。这些任务是分时完成的,有操作系统来完成各个应用在运行时的任务切换。并发性虽然能有效改善系统资源的利用率,但并发性也带来了对共享资源的争夺问题,即同步互斥问题;执行时间的不确定性问题,即并发程序在执行中是走走停停,断续推进的。并发性对操作系统的设计也带来了很多挑战,一不小心就会出现程序执行结果不确定,程序死锁等很难调试和重现的问题。 36 | 37 | \paragraph{异步性} 38 | 39 | 在这里,异步是指由于操作系统的调度和中断等,会不时地暂停或打断当前正在运行的程序,使得程序的整个运行过程走走停停。在应用程序运行的表现上,特别它的执行完成时间是不可预测的。但需要注意,只要应用程序的输入是一致的,那么它的输出结果应该是符合预期的。 40 | 41 | \paragraph{共享性} 42 | 43 | 共享是指多个应用并发运行时,宏观上体现出它们可同时访问同一个资源,即这个资源可被共享。但其实在微观上,操作系统在硬件等的支持下要确保应用程序互斥或交替访问这个共享的资源。比如两个应用同时写访问同一个内存单元,从宏观的应用层面上看,二者都能正确地读出同一个内存单元的内容。而在微观上,操作系统会调度应用程序的先后执行顺序,在数据总线上任何一个时刻,只有一个应用去访问存储单元。 44 | 45 | \paragraph{持久性} 46 | 47 | 操作系统提供了文件系统来从可持久保存的存储介质(硬盘,SSD等,以后以硬盘来代表)中取数据和代码到内存中,并可以把内存中的数据写回到硬盘上。硬盘在这里是外设,具有持久性,以文件系统的形式呈现给应用程序。 48 | 49 | 文件系统也可看成是操作系统对硬盘的虚拟化 50 | 51 | 这种持久性的特征进一步带来了共享属性,即在文件系统中的文件可以被多个运行的程序所访问,从而给应用程序之间实现数据共享提供了方便。即使掉电,磁盘上的数据还不会丢失,可以在下一次机器加电后提供给运行的程序使用。持久性对操作系统的执行效率提出了挑战,如何让数据在高速的内存和慢速的硬盘间高效流动是需要操作系统考虑的问题。 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /latex/os_history.tex: -------------------------------------------------------------------------------- 1 | \subsection{操作系统历史} 2 | 3 | 在大众的眼中,操作系统就是他们的手机/终端上的软件系统,包括各种应用程序集合,但在历史上,操作系统也是从无到有地逐步发展起来的。操作系统主要完成对硬件控制和对应用程序的服务所必需的功能,操作系统的历史与计算机发展的历史密不可分。操作系统的内涵和功能随着历史的发展也在一直变化,改进中,在今天,没有图形界面和各种文件浏览器已经不能称为一个操作系统了。 4 | 5 | \subsubsection{三叶虫时代} 6 | 7 | 计算机在最开始出现的时候是没有操作系统的。启动,扳开关,装卡片/纸带等比较辛苦的工作都是计算机操作员(Operator)或者用户自己完成。操作员/用户带着记录有程序和数据的卡片(punch card)或打孔纸带去操作机器。装好卡片/纸带后,启动卡片/纸带阅读器,让计算机把程序和数据读入计算机机的内存中后,计算机就开始工作,并把结果也输出到卡片/纸带或显示屏上,最后程序停止。 8 | 9 | 由于人的操作效率太低,计算机的机时宝贵,所以就引入监控程序(Monitor)辅助完成输入,输出,加载,运行程序等工作,这是现代操作系统的起源。一般情况下,计算机每次只能执行一个任务,CPU大部分时间都在等待人的缓慢操作。 10 | 11 | \subsubsection{恐龙时代} 12 | 13 | 早期的操作系统非常多样化,专用化,生产商生产出针对各自硬件的专用操作系统,大部分用汇编语言编写,这导致操作系统的进化比较缓慢,但进化再持续。在1964年,IBM公司开发了面向System/360系列机器的统一可兼容的操作系统——OS/360。OS/360是一种批处理操作系统。为了能充分地利用计算机系统,应尽量使该系统连续运行,减少空闲时间,所以批处理操作系统把一批作业(古老的术语,可理解为现在的程序)以脱机方式输入到磁带上,并使这批作业能一个接一个地连续处理:1)将磁带上的一个作业装入内存;2)并把运行控制权交给该作业;3)当该作业处理完成后,把控制权交还给操作系统;4)重复1-3的步骤。 14 | 15 | 批处理操作系统分为单道批处理系统和多道批处理系统。单道批处理操作系统只能管理内存中的一个(道)作业,无法充分利用计算机系统中的所有资源,致使系统整体性能较差。多道批处理操作系统能管理内存中的多个(道)作业,可比较充分地利用计算机系统中的所有资源,提升系统整体性能。二者的共同特点是人机交互性差,这对修改和调试程序很不方便。 16 | 17 | \subsubsection{哺乳动物时代} 18 | 19 | 20世纪60年代末,提高人机交互方式的分时操作系统越来越展露头角。分时是指多个用户和多个程序以很小的时间间隔来共享使用同一台计算机上的CPU和其他硬件/软件资源。1964年由贝尔实验室、麻省理工学院及美国通用电气公司所共同参与研发目标远大的MULTICS(MULTiplexed Information and Computing System)操作系统,MULTICS是一套安装在大型主机上多人多任务的操作系统。 MULTICS以兼容分时系统(CTSS)做基础,建置在美国通用电力公司的大型机GE-645,目标是连接1000部终端机,支持300的用户同时上线。因MULTICS项目的工作进度过于缓慢,1969年AT\&T的 Bell 实验室从MULTICS 研发中撤出。但贝尔实验室的两位软件工程师 Thompson 与 Ritchie借鉴了一些重要的Multics理念,以C语言为基础,发展出UNIX操作操作系统。UNIX操作系统的早期版本是完全免费的,可以轻易获得并随意修改,所以它得到了广泛的接受。后来,它成为开发小型机操作系统的起点。由于早期的广泛应用,它已经成为的分时操作系统的典范。 20 | 21 | \subsubsection{古猿人时代} 22 | 23 | 20世纪70年代,微型处理器的发展使计算机的应用普及至中小企及个人爱好者,推动了个人计算机(Personal Computer)的发展,也进一步推动了面向个人使用的操作系统的出现。其代表是由微软公司中在20世纪80年代为个人计算机开发的DOS/Windows操作系统,其特点是简单易用,特别是基于Windows操作系统的GUI界面,极大地简化了一般用户使用计算机的难度,使得计算机得到了快速的普及。这里需要注意的是,第一个带GUI界面的个人计算机原型起源于伟大却又让人扼腕叹息的施乐帕洛阿图研究中心PARC(Palo Alto Research Center),PARC研发出的带有图标、弹出式菜单和重叠窗口的GUI(Graphical User Interface),可利用鼠标的点击动作来进行操控,这是当今我们所使用的GUI系统的基础。 24 | 25 | \subsubsection{智人时代} 26 | 27 | 智人时代的操作系统的代表是Linux操作系统内核(Linux kernel),它覆盖了工业控制、物联网、移动终端、桌面计算机、服务器、数据中心到超级计算机的各个领域。其中面向移动终端的Android操作系统基于Linux kernel,已成为21世纪个人终端操作系统的代表之一,Linux kernel在巨型机到数据中心服务器操作系统中也占据了统治地位。 28 | 29 | 1991年8 月,芬兰学生 Linus Torvalds(中文名:林纳斯·托瓦兹)在 comp.os.minix 新闻组贴上了以下这段话:  30 | 31 | "你好,所有使用 minix 的人 -我正在为 386 ( 486 ) AT 做一个免费的操作系统 ( 只是为了爱好 )......" 32 | 33 | 而他所说的"爱好″就变成我们今天知道的 Linux kernel。 Linus Torvalds采用了GPL版权协议,通过Internet发布了 Linux kernel的源代码。在Internet的日渐盛行以及 Linux 开放自由的GPL版权之下,吸引了无数计算机Hacker和公司投入开发、改善Linux kernel,使得 Linux kernel的功能日见强大并被广泛使用。  34 | 35 | \subsubsection{神人时代} 36 | 37 | 当前,大数据、人工智能、机器学习、高速网络、AR/VR对操作系统等系统软件带来了新的挑战。如何有效支持和利用这些技术是未来操作系统的方向。\emph{注:面向此时代的操作系统目前还未出现。} 38 | 39 | 40 | -------------------------------------------------------------------------------- /latex/os_interface.tex: -------------------------------------------------------------------------------- 1 | \subsection{操作系统接口} 2 | 3 | 首先,读者可站在使用操作系统的角度来看操作系统。操作系统内核是一个需要提供各种服务的软件,其服务对象是应用程序,而用户(这里可以理解为一般使用计算机的人)是通过应用程序的服务间接获得操作系统的服务的),所以操作系统内核藏在一般用户看不到的地方。但应用程序需要访问操作系统,获得操作系统的服务,这就需要通过操作系统的接口才能完成。如果把操作系统看成是一个函数库,那么其接口就是函数名称和它的参数。但操作系统不是简单的一个函数库,它的接口需要考虑安全因素,使得应用软件不能直接读写操作系统内部函数的地址地址空间,为此,操作系统设计了一个安全可靠的接口,我们称为系统调用接口(System Call Interface),应用程序可以通过系统调用接口请求获得操作系统的服务,但不能直接调用操作系统的函数和全局变量;操作系统提供完服务后,返回应用程序继续执行。 4 | 5 | 对于实际操作系统而言,具有大量的服务接口,比如Linux有上百个系统调用接口。为了简单起见,以ucore OS为例,可以看到它为应用程序提供了如下一些接口: 6 | 7 | \begin{enumerate} 8 | \item 9 | \item 进程管理:复制创建--fork、退出--exit、执行--exec、... 10 | \item 同步互斥的并发控制:信号量--semaphore、管程--monitor、条件变量--condition variable 、... 11 | \item 进程间通信:管道--pipe、信号--signal、事件--event、邮箱--mailbox、共享内存--shared mem、... 12 | \item 文件I/O操作:读--read、写--write、打开--open、关闭--close、... 13 | \item 外设I/O操作:外设包括键盘、显示器、串口、磁盘、时钟、...,但接口是直接采用了文件I/O操作的系统调用接口 14 | \end{enumerate} 15 | 16 | 17 | 这在某种程度上说明了文件是外设的一种抽象。在UNIX中(ucore是模仿UNIX),大部分外设都可以以文件的形式来访问。 18 | 有了这些接口,简单的应用程序就不用考虑底层硬件细节,可以在操作系统的服务支持和管理下简洁地完成其应用功能了。 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /latex/osprinciple_control_computer.tex: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /latex/overlook.tex: -------------------------------------------------------------------------------- 1 | \section{初步了解操作系统} 2 | 3 | 我们可以把软件分成应用软件和系统软件。所谓应用软件,即完成某种特定应用功能的软件,比如写文档的office软件,玩游戏的游戏软件等。所谓系统软件,即完成系统功能的软件,这里的系统功能相对与特定应用功能而言,更加底层和通用,比如编译器,C运行时库,操作系统等。而对于系统软件,我们又可以分为系统应用((编译器,C运行时库等)和操作系统。这里把操作系统单独分出来,是由于操作系统直接管理了硬件,所有的应用都需要操作系统的支持,才能正常工作。 4 | 5 | 操作系统其实是一个相比较复杂的系统软件,直接管理计算机硬件和各种外设,以及给应用软件提供帮助。这样描述还太简单了一些,我们可对其进一步描述:操作系统是一个可以管理CPU、内存和各种外设,并管理和服务应用软件的软件。为了完成这些工作,操作系统需要知道如何与硬件打交道,如何更好地服务好应用软件。 6 | 7 | \input{os_history} 8 | \input{helloworld} 9 | \input{os_define} 10 | \input{os_interface} 11 | \input{os_abstract} 12 | \input{os_feature} -------------------------------------------------------------------------------- /latex/page_alloc_algorithm.tex: -------------------------------------------------------------------------------- 1 | \section{【原理】页内存分配算法}\label{ux539fux7406ux9875ux5185ux5b58ux5206ux914dux7b97ux6cd5} 2 | 3 | 在proj5中进行在动态分配内存时,存在很多限制,效率很低。在操作系统原理中,为了有效地分配内存,首先需要了解和跟踪空闲内存和分布情况,一般可采用位图(bit 4 | map)和双向链表两种方式跟踪内存使用情况。若采用位图方式,则每个页对应位图区域的一个bit,如果此位为0,表示空闲,如果为1,表示被占用。采用位图方式很省空间,但查找n个长度为0的位串的开销比较大。而双向链表在查询或修改操作方面灵活性和效率较高,所以ucore采用双向链表来跟踪跟踪内存使用情况。 5 | 6 | 假设整个物理内存空闲空间的以页为单位被一个双向链表管理起来,每个表项管理一个物理页。这需要设计某种算法来查找空闲页和回收空闲页。ucore实现了首次适配(first 7 | fit)算法、最佳适配(best fit)算法、最差适配(worst 8 | fit)算法和兄弟(buddy)算法,这些算法都可以实现在ucore提供的物理内存页管理器框架pmm\_manager下。 9 | 10 | 首次适配(first 11 | fit)算法的分配内存的设计思路是物理内存页管理器顺着双向链表进行搜索空闲内存区域,直到找到一个足够大的空闲区域,这是一种速度很快的算法,因为它尽可能少地搜索链表。如果空闲区域的大小和申请分配的大小正好一样,则把这个空闲区域分配出去,成功返回;否则将该空闲区分为两部分,一部分区域与申请分配的大小相等,把它分配出去,剩下的一部分区域形成新的空闲区。其释放内存的设计思路很简单,只需把这块区域重新放回双向链表中即可。 12 | 13 | 最佳适配(best 14 | fit)算法的设计思路是物理内存页管理器搜索整个双向链表(从开始到结束),找出能够满足申请分配的空间大小的最小空闲区域。找到这个区域后的处理以及释放内存的处理与上面类似。最佳适配算法试图找出最接近实际需要的空闲区,名字上听起来很好,其实在查询速度上较慢,且较易产生多的内存碎片。 15 | 16 | 最差适配(worst fit)算法与最佳适配(best 17 | fit)算法的设计思路相反,物理内存页管理器搜索整个双向链表,找出能够满足申请分配的空间大小的最大空闲区域,使新的空闲区比较大从而可以继续使用。在实际效果上,查询速度上也较慢,产生内存碎片相对少些。 18 | 19 | 上述三种算法在实际应用中都会产生碎片较多,效率不高的问题。为此一般操作系统会采用buddy算法来改进上述问题。buddy算法的基本设计思想是:在buddy系统中,被占用的内存空间和空闲内存空间的大小均为2的k次幂(k是正整数)。这样在ucore中,若申请n个页的内存空间,则实际可能分配的空间大小为2K个页(2k-1\textless{}n\textless{}=2k)。若初始化时的空闲内存空间容量为2m个页,这空闲块的大小只可能是20、21、\ldots{}、2m个页。 20 | 21 | \begin{figure}[htbp] 22 | \centering 23 | \includegraphics{figures/6.png} 24 | \caption{6} 25 | \end{figure} 26 | 27 | 假定内存一开始是一个连续地址空间(大小为2\^{}k个页)的大空闲块,且最小分配单位为1个页(4KB),则buddy 28 | system初始化时将生成一个长度为k + 1的可用空间表List, 29 | 并将全部可用空间作为一个大小为2\^{}k个页的空闲块Bk挂接在空闲块数组链表List的最后一个节点上, 30 | 如下图: 31 | 32 | \begin{figure}[htbp] 33 | \centering 34 | \includegraphics{figures/7.png} 35 | \caption{7} 36 | \end{figure} 37 | 38 | 当ucore其他子系统申请n个字节的存储空间时, buddy 39 | system分配的空闲块大小为2\^{} m个页,m满足条件:2\^{} (m-1) \textless{} 40 | n \textless{}= 2\^{} m 41 | 42 | 此时buddy 43 | system将在list中的m位置寻找可用的空闲块。初始化时List中这个位置为空, 44 | 于是buddy system就向上查找m+1,\ldots{},直到达到k位置为止. 找到k位置后, 45 | 便得到可用空闲块Bk, 46 | 此时Bk将分裂成两个大小为2\^{}(k-1)的空闲块Bk-1a和Bk-1b, 47 | 并将其中一个插入到List中k-1位置, 同时对另外一个继续进行分裂. 48 | 如此以往直到得到两个大小为2\^{}m个页的块为止,并把其中一个空闲块分配给需求方。此时的内存如下图所示: 49 | 50 | \begin{figure}[htbp] 51 | \centering 52 | \includegraphics{figures/8.png} 53 | \caption{8} 54 | \end{figure} 55 | 56 | 如果buddy system在运行一段时间之后, List中某个位置t可能会出现多个块, 57 | 则将其他块依次链接可用块链表的末尾。当buddy system要在t位置取可用块时, 58 | 直接从链表头取一个即可。 59 | 60 | 当一个存储块被释放时, buddy 61 | system将把此内存块回收到空闲块链表List中。此时buddy 62 | system系统将根据此存储块的大小计算出其在List中的位置, 63 | 然后插入到空闲块链表的末尾。在这一步完成后, 系统立即开始合并尝试操作, 64 | 该操作是将地址相邻且大小相等的空闲块(简称buddy,即``伙伴''空闲块)合并到一起, 65 | 形成一个更大的空闲块,并重新放到空闲块链表List的对应位置中, 66 | 并继续对更大的块进行合并, 直到无法合并为止。 67 | 68 | 严蔚敏老师的``数据结构''一书第8章第4节对buddy算法有详尽的解释,``understanding 69 | linux kernel''此书对此也有很好的描述,读者可以进一步参考。 70 | 71 | 对于上述4个内存分配算法,可参考对应的proj5.1/5.1.1/5.1.2/5.2中的kern/mm/*\_pmm.{[}ch{]}的具体实现来进一步了解。 72 | 73 | \lstinline!(可以进一步描述三种算法的具体实现)! 74 | -------------------------------------------------------------------------------- /latex/pages_mem_managment.tex: -------------------------------------------------------------------------------- 1 | \section{【原理】分页内存管理}\label{ux539fux7406ux5206ux9875ux5185ux5b58ux7ba1ux7406} 2 | 3 | 在分页内存管理中,一方面把实际物理内存(也称主存)划分为许多个固定大小的内存块,称为物理页面,或者是页框(page 4 | frame);另一方面又把CPU(包括程序员)看到的虚拟地址空间也划分为大小相同的块,称为虚拟页面,或者简称为页面、页(page)。页面的大小要求是2的整数次幂,一般在256个字节到4M字节之间。在本书中,页面的大小设定为4KB。在32位的86x86中,虚拟地址空间是4GB,物理地址空间也也是4GB,因此在理论上程序可访问到1M个虚拟页面和1M个物理页面。软件的每一物理页面都可以放置在主存中的任何地方,分页系统(需要CPU等硬件系统提供相应的分页机制硬件支持,详见下一节)提供了程序中使用的虚地址和主存中的物理地址之间的动态映射。这样当程序访问一个虚拟地址时,支持分页机制的相关硬件自动把CPU访问的虚拟地址虚拟地址拆分为页号(可能有多级页号)和页内偏移量,再把页号映射为页帧号,最后加上页内偏移组成一个物理地址,这样最终完成对这个地址的读/写/执行等操作。 5 | 6 | 假设程序在运行时要去读地址0x100的内容到寄存器1(用REG1表示)中,执行如下的指令: 7 | 8 | \begin{lstlisting} 9 | mov 0x100, REG1 10 | \end{lstlisting} 11 | 12 | 虚拟地址0x100被发送给CPU内部的内存管理单元(MMU),然后MMU通过支持分页机制的相关硬件逻辑就会把这个虚拟地址是位于第0个虚拟页面当中(设页大小为4KB),页内偏移是0x100;而操作系统的分页管理子系统已经设置好第0个虚拟页面对应的是第2个物理页帧,物理页帧的起始地址是0x2000,然后再加上页内的偏移地址0x100,所以最后得到的物理地址就是0x2100。然后MMU就会把这个真正的物理地址发送到计算机系统中的地址总线上,从而可正确访问相应的物理内存单元。 13 | 14 | 如果操作系统的分页管理子系统没有设置第0个虚拟页面对应的物理页帧,则表示第0个虚拟页面当前没有对应的物理页帧,这会导致CPU产生一个缺页异常,由操作系统的缺页处理服务例程来选择如何处理。如果缺页处理服务例程认为这是一次非法访问,它将报错,终止软件运行;如果它认为是一次合理的访问,则它会采用分配物理页等手段建立正确的页映射,使得能够重新正确执行产生异常的访存指令。 15 | -------------------------------------------------------------------------------- /latex/phymem_analysis.tex: -------------------------------------------------------------------------------- 1 | \section{【实现】物理内存探测}\label{ux5b9eux73b0ux7269ux7406ux5185ux5b58ux63a2ux6d4b} 2 | 3 | 物理内存探测是在bootasm.S中实现的,相关代码很短,如下所示: 4 | 5 | \begin{lstlisting} 6 | probe_memory: 7 | //对0x8000处的32位单元清零,即给位于0x8000处的 8 | //struct e820map的结构域nr_map清零 9 | movl $0, 0x8000 10 | xorl %ebx, %ebx 11 | //表示设置调用INT 15h BIOS中断后,BIOS返回的映射地址描述符的起始地址 12 | movw $0x8004, %di 13 | start_probe: 14 | movl $0xE820, %eax // INT 15的中断调用参数 15 | //设置地址范围描述符的大小为20字节,其大小等于struct e820map的结构域map的大小 16 | movl $20, %ecx 17 | //设置edx为534D4150h (即4个ASCII字符“SMAP”),这是一个约定 18 | movl $SMAP, %edx 19 | //调用int 0x15中断,要求BIOS返回一个用地址范围描述符表示的内存段信息 20 | int $0x15 21 | //如果eflags的CF位为0,则表示还有内存段需要探测 22 | jnc cont 23 | //探测有问题,结束探测 24 | movw $12345, 0x8000 25 | jmp finish_probe 26 | cont: 27 | //设置下一个BIOS返回的映射地址描述符的起始地址 28 | addw $20, %di 29 | //递增struct e820map的结构域nr_map 30 | incl 0x8000 31 | //如果INT0x15返回的ebx为零,表示探测结束,否则继续探测 32 | cmpl $0, %ebx 33 | jnz start_probe 34 | finish_probe: 35 | \end{lstlisting} 36 | 37 | 上述代码正常执行完毕后,在0x8000地址处保存了从BIOS中获得的内存分布信息,此信息按照struct 38 | e820map的设置来进行填充。这部分信息将在bootloader启动ucore后,由ucore的page\_init函数来根据struct 39 | e820map的memmap(定义了起始地址为0x8000)来完成对整个机器中的物理内存的总体管理。 40 | -------------------------------------------------------------------------------- /latex/phymen_size.tex: -------------------------------------------------------------------------------- 1 | \section{【背景】探测计算机系统中的物理内存分布和大小}\label{ux80ccux666fux63a2ux6d4bux8ba1ux7b97ux673aux7cfbux7edfux4e2dux7684ux7269ux7406ux5185ux5b58ux5206ux5e03ux548cux5927ux5c0f} 2 | 3 | 在proj5中,操作系统需要知道了解整个计算机系统中的物理内存如何分布的,哪些被可用,哪些不可用。其基本方法是通过BIOS中断调用来帮助完成的。其中BIOS中断调用必须在实模式下进行,所以在bootloader进入保护模式前完成这部分工作相对比较合适。这些部分由boot/bootasm.S中从probe\_memory处到finish\_probe处的代码部分完成完成。通过BIOS中断获取内存可调用参数为e820h的INT 4 | 15h BIOS中断。BIOS通过系统内存映射地址描述符(Address Range 5 | Descriptor)格式来表示系统物理内存布局,其具体表示如下: 6 | 7 | \begin{lstlisting} 8 | Offset  Size    Description      9 | 00h    8字节   base address    #系统内存块基地址 10 | 08h    8字节   length in bytes #系统内存大小 11 | 10h    4字节   type of address range #内存类型 12 | \end{lstlisting} 13 | 14 | 看下面的(Values for System Memory Map address type) Values for System 15 | Memory Map address type: 16 | 17 | \begin{lstlisting} 18 | 01h memory, available to OS 19 | 02h reserved, not available (e.g. system ROM, memory-mapped device) 20 | 03h ACPI Reclaim Memory (usable by OS after reading ACPI tables) 21 | 04h ACPI NVS Memory (OS is required to save this memory between NVS sessions) 22 | other not defined yet -- treat as Reserved 23 | \end{lstlisting} 24 | 25 | INT15h BIOS中断的详细调用参数: 26 | 27 | \begin{lstlisting} 28 | eax:e820h:INT 15的中断调用参数; 29 | edx:534D4150h (即4个ASCII字符“SMAP”) ,这只是一个签名而已; 30 | ebx:如果是第一次调用或内存区域扫描完毕,则为0。 如果不是,则存放上次调用之后的计数值; 31 | ecx:保存地址范围描述符的内存大小,应该大于等于20字节; 32 | es:di:指向保存地址范围描述符结构的缓冲区,BIOS把信息写入这个结构的起始地址。 33 | \end{lstlisting} 34 | 35 | 此中断的返回值为: 36 | 37 | \begin{lstlisting} 38 | cflags的CF位:若INT 15中断执行成功,则不置位,否则置位; 39 | eax:534D4150h ('SMAP') ; 40 | es:di:指向保存地址范围描述符的缓冲区,此时缓冲区内的数据已由BIOS填写完毕 41 | ebx:下一个地址范围描述符的计数地址 42 | ecx :返回BIOS往ES:DI处写的地址范围描述符的字节大小 43 | ah:失败时保存出错代码 44 | \end{lstlisting} 45 | 46 | 这样,我们通过调用INT 15h 47 | BIOS中断,递增di的值(20的倍数),让BIOS帮我们查找出一个一个的内存布局entry,并放入到一个保存地址范围描述符结构的缓冲区中,供后续的ucore进一步进行物理内存管理。这个缓冲区结构定义在memlayout.h中: 48 | 49 | \begin{lstlisting} 50 | struct e820map { 51 | int nr_map; 52 | struct { 53 | long long addr; 54 | long long size; 55 | long type; 56 | } map[E820MAX]; 57 | }; 58 | \end{lstlisting} 59 | 60 | -------------------------------------------------------------------------------- /latex/prerequisite.tex: -------------------------------------------------------------------------------- 1 | \section{预备知识} 2 | 本书希望通过设计实现操作系统来更好地理解操作系统原理和概念。设计实现操作系统其实就是设计实现一个可以管理CPU、内存和各种外设,并管理和服务应用软件的系统软件。为此还是需要先了解一些基本的计算机原理和编程的知识。本书的例子和描述需要读者学习过计算机原理课程、程序设计课程,掌握C语言编程(了解指针等的编程)。如需完成基于RISC-V的ucore实验,则对基于RISC-V的体系结构有一定的了解,大致了解RISC-V的汇编语言。 3 | -------------------------------------------------------------------------------- /latex/privilege_level.tex: -------------------------------------------------------------------------------- 1 | \section{【背景】分段机制的特权限制}\label{ux80ccux666fux5206ux6bb5ux673aux5236ux7684ux7279ux6743ux9650ux5236} 2 | 3 | 在保护模式下,特权级总共有4个,编号从0(最高特权)到3(最低特权)。三类主要的资源,即内存、I/O地址空间以及特权指令需要保护。特权指令如果被用户态的程序所使用,就会受到保护模式的保护机制限制,导致一个故障中断(general-protection 4 | exception)。对内存和I/O端口的访问存在类似的特权级限制。为了更好地理解不同特权级,这里先介绍三个概念 5 | 6 | \begin{itemize} 7 | \item 8 | CPL:当前特权级(Current Privilege Level) 9 | 保存在CS段寄存器(选择子)的最低两位,CPL就是当前活动代码段的特权级,并且它定义了当前所执行程序的特权级别) 10 | \item 11 | DPL:描述符特权(Descriptor Privilege Level) 12 | 存储在段描述符中的权限位,用于描述对应段所属的特权等级,也就是段本身真正的特权级。 13 | \item 14 | RPL:请求特权级RPL(Request Privilege Level) 15 | RPL保存在选择子的最低两位。RPL说明的是进程对段访问的请求权限,意思是当前进程想要的请求权限。RPL的值由程序员自己来自由的设置,并不一定RPL\textgreater{}=CPL,但是当RPL\textless{}CPL时,实际起作用的就是CPL了,因为访问时的特权检查是判断:max(RPL,CPL)\textless{}=DPL是否成立,所以RPL可以看成是每次访问时的附加限制,RPL=0时附加限制最小,RPL=3时附加限制最大。 16 | \end{itemize} 17 | -------------------------------------------------------------------------------- /latex/process_schedule.tex: -------------------------------------------------------------------------------- 1 | \section{进程调度}\label{ux8fdbux7a0bux8c03ux5ea6} 2 | 3 | \subsection{实验目标}\label{ux5b9eux9a8cux76eeux6807} 4 | 5 | 在只有一个或几个CPU的计算机系统中,进程数量一般远大于CPU数量,CPU是一个稀缺资源,多个进程不得不分时占用CPU执行各自的工作,操作系统必须提供一种手段来保证各个进程``和谐''地共享CPU。为此,需要在lab3的基础上设计实现ucore进程调度类框架以便于设计各种进程调度算法,同时以提高计算机整体效率和尽量保证各个进程``公平''地执行作为目标来设计各种进程调度算法。 6 | 7 | \subsection{proj13/13.1/13.2概述}\label{proj1313.113.2ux6982ux8ff0} 8 | 9 | \subsubsection{实现描述}\label{ux5b9eux73b0ux63cfux8ff0} 10 | 11 | project13是lab4的第一个项目,它基于lab3的最后一个项目proj12。主要参考了Linux-2.6.23设计了一个简化的进程调度类框架,能够在此框架下实现不同的调度算法,并在此框架下实现了一个简单的FIFO调度算法。接下来的proj13.1在此调度框架下实现了轮转(Round 12 | Robin,简称RR)调度算法,proj13.2在此调度框架下实现了多级反馈队列(Multi-Level 13 | Feed Back,简称MLFB)调度算法。 14 | 15 | \subsubsection{项目组成}\label{ux9879ux76eeux7ec4ux6210} 16 | 17 | \begin{lstlisting} 18 | proj13 19 | ├── kern 20 | │   ├── ...... 21 | │   ├── process 22 | │   │   ├──…… 23 | │   │   ├── proc.h 24 | │   │   └── proc.c 25 | │   ├── schedule 26 | │   │   ├── sched.c 27 | │   │   ├── sched_FCFS.c 28 | │   │   ├── sched_FCFS.h 29 | │   │   └── sched.h 30 | │   └── trap 31 | │   ├── trap.c 32 | │   └── …… 33 | └── user 34 | ├── matrix.c 35 | └── …… 36 | 37 | 17 directories, 129 files 38 | \end{lstlisting} 39 | 40 | 相对与proj12,proj13增加了3个文件,修改了相对重要的5个文件。主要修改和增加的文件如下: 41 | 42 | \begin{itemize} 43 | \item 44 | process/proc.{[}ch{]}:扩展了进程控制块的定义能够把处于就绪态的进程放入到一个就绪队列中,并在创建进程时对新扩展的成员变量进行初始化。 45 | \item 46 | schedule/sched.{[}ch{]}:增加进程调度类的定义和相关进程调度类的共性函数。 47 | \item 48 | schedule/sched\_FCFS.{[}ch{]}:基于进程调度类的FCFS调度算法实例的设计实现; 49 | \item 50 | schedule/sched.{[}ch{]}:实现了一个先来先服务(First Come First 51 | Serve)策略的进程调度。 52 | \item 53 | user/matrix.c:矩阵乘用户测试程序 54 | \end{itemize} 55 | 56 | \subsubsection{编译运行}\label{ux7f16ux8bd1ux8fd0ux884c} 57 | 58 | 编译并运行proj13的命令如下: 59 | 60 | \begin{lstlisting} 61 | make 62 | make qemu 63 | \end{lstlisting} 64 | 65 | 则可以得到如下显示界面 66 | 67 | \begin{lstlisting} 68 | (THU.CST) os is loading ... 69 | 70 | Special kernel symbols: 71 | …… 72 | ++ setup timer interrupts 73 | kernel_execve: pid = 3, name = "matrix". 74 | fork ok. 75 | pid 4 is running (1000 times)!. 76 | pid 4 done!. 77 | pid 5 is running (1400 times)!. 78 | pid 5 done!. 79 | …… 80 | pid 22 is running (33400 times)!. 81 | pid 22 done!. 82 | pid 23 is running (33400 times)!. 83 | pid 23 done!. 84 | matrix pass. 85 | all user-mode processes have quit. 86 | init check memory pass. 87 | kernel panic at kern/process/proc.c:456: 88 | initproc exit. 89 | 90 | Welcome to the kernel debug monitor!! 91 | Type 'help' for a list of commands. 92 | K> 93 | \end{lstlisting} 94 | 95 | 这其实是在采用简单的FCFS调度方法来执行matrix用户进程,这个matrix进程将创建20个进程来各自执行二维矩阵乘的工作。Ucore将按照FCFS的调度方法,一个一个地按创建顺序执行每个子进程。一个子进程结束后,再调度另外一个子进程运行。这实际上看不出ucore是如何具体实现进程调度类框架和FCFS调度算法的。下面我们首先介绍一下进程调度的基本原理,然后再分析ucore的调度框架和调度算法的实现。 96 | -------------------------------------------------------------------------------- /latex/process_status_change.tex: -------------------------------------------------------------------------------- 1 | \section{进程运行状态转变过程}\label{ux8fdbux7a0bux8fd0ux884cux72b6ux6001ux8f6cux53d8ux8fc7ux7a0b} 2 | 3 | 分析完从进程/线程从创建到退出的整个过程,我们需要在从全局的角度来看看进程/线程在做整个运行过程中的运行状态转变过程。在执行状态转变过程中,ucore在调度过程总,并没有区分线程和进程,所以进程和线程的执行状态转变是一致的,分析的结果适合用户线程和用户进程的执行过程。 4 | 5 | 首先为了描述进程/线程的整个状态集合,ucore在kern/process/proc.h中定义了进程/线程的运行状态: 6 | 7 | \begin{lstlisting} 8 | // process's state in his life cycle 9 | enum proc_state { 10 | PROC_UNINIT = 0,  // uninitialized 11 | PROC_SLEEPING,    // sleeping 12 | PROC_RUNNABLE,    // runnable(maybe running) 13 | PROC_ZOMBIE,      // almost dead, and wait parent proc to reclaim his resource 14 | }; 15 | \end{lstlisting} 16 | 17 | 这与操作系统原理讲解的五进程执行状态相比,少了一个PROC\_RUNNING态(表示正在占用CPU执行),这是由于在ucore中,用current(基于proc\_strcut数据结构)进程控制块指针指向了当前正在运行的进程/线程PROC\_RUNNING态,所以就没必要再增加一个PROC\_RUNNING态了。那么那些事件或内核函数会触发状态的转变呢?通过分析uore源码,我们可以得到如下表示: 18 | 19 | \begin{figure}[htbp] 20 | \centering 21 | \includegraphics{figures/0_4.png} 22 | \caption{0\_5} 23 | \end{figure} 24 | 25 | 当父进程得到子进程的通知,回收完子进程控制块所占内存后,这个进程就彻底消失了。我们也可以用一个类似有限状态自动机来表示状态的变化:\lstinline!(需要用visio重画)! 26 | 27 | \begin{lstlisting} 28 | process state changing: 29 | 30 | alloc_proc                                 RUNNING 31 | +                                   +--<----<--+ 32 | +                                   + proc_run + 33 | V                                   +-->---->--+  34 | PROC_UNINIT -- proc_init/wakeup_proc --> PROC_RUNNABLE -- try_free_pages/do_wait/do_sleep --> PROC_SLEEPING -- 35 | A      +                                                           + 36 | |      +--- do_exit --> PROC_ZOMBIE                                + 37 | +                                                                  +  38 | -----------------------wakeup_proc---------------------------------- 39 | \end{lstlisting} 40 | 41 | -------------------------------------------------------------------------------- /latex/process_summary.tex: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /latex/proj4_1_1_kernel_switch_to_user.tex: -------------------------------------------------------------------------------- 1 | \section{可在内核态和用户态之间进行切换的ucore}\label{ux53efux5728ux5185ux6838ux6001ux548cux7528ux6237ux6001ux4e4bux95f4ux8fdbux884cux5207ux6362ux7684ucore} 2 | 3 | 在操作系统原理中,一直强调操作系统运行在内核态(特权态),应用程序运行在用户态(非特权态)。但为什么说处于用户态的应用程序就不能访问内核态的数据,而内核态的操作系统可以访问用户态的数据?我们有没有一个project来体验内核态和用户态的区别是什么?更进一步体验如何在内核态和用户态之间进行切换呢?project4.1.1为此进行了尝试。 4 | 5 | \subsection{实验目标}\label{ux5b9eux9a8cux76eeux6807} 6 | 7 | 通过学习和实践,读者可以了解如何CPU处不同特权态下的执行特点和限制,理解如何从内核态切换到用户态,以及如何从用户态切换到内核态。 8 | 9 | \subsection{proj4.1.1概述}\label{proj4.1.1ux6982ux8ff0} 10 | 11 | \subsubsection{实现描述}\label{ux5b9eux73b0ux63cfux8ff0} 12 | 13 | proj4.1.1建立在proj4.1(当然是基于proj4)基础之上,主要完成了用户态(非特权态)与内核态相互切换的过程。相对于proj4,主要增加了两部分工作,一部分是从用户态返回内核态的准备工作,即建立任务段(Task 14 | Segment)和任务段描述符(SEG\_TSS),设置陷阱中断号(T\_SWITCH\_TOK)和对应的中断处理例程。另外一部分是对内核栈进行各种特殊处理,使得能够完成内核态切换到用户态或用户态切换到内核态的工作。 15 | 16 | \subsubsection{项目组成}\label{ux9879ux76eeux7ec4ux6210} 17 | 18 | 这里我们通过proj4.1.1来完成此事。proj4.1.1整体目录结构如下所示: 19 | 20 | \begin{lstlisting} 21 | proj4.1.1 22 | |-- kern 23 | | |-- init 24 | | | `-- init.c 25 | | |-- mm 26 | | | |-- memlayout.h 27 | | | |-- mmu.h 28 | | | |-- pmm.c 29 | | | `-- pmm.h 30 | | `-- trap 31 | | |-- trap.c 32 | | |-- trapentry.S 33 | | |-- trap.h 34 | | `-- vectors.S 35 | …… 36 | \end{lstlisting} 37 | 38 | 相对于proj4,改动不多,主要修改和增加的文件如下: 39 | 40 | \begin{itemize} 41 | \item 42 | memlayout.h:定义了全局描述符的索引值和一些段描述符的属性。 43 | \item 44 | pmm.{[}ch{]}:为了能够使CPU从用户态转换到内核态,在ucore初始化时,设置任务段和任务段描述符,重新加载任务段和段描述符表; 45 | \item 46 | trap.c:设置自定义的陷阱中断T\_SWITCH\_TOK(用于用户态切换到内核态)和实现对自定义的陷阱中断T\_SWITCH\_TOK/T\_SWITCH\_TOU的中断处理例程,使得CPU能够在内核态和用户态之间切换。 47 | \end{itemize} 48 | 49 | \subsubsection{编译运行}\label{ux7f16ux8bd1ux8fd0ux884c} 50 | 51 | 编译并运行proj4.1.1的命令如下: 52 | 53 | \begin{lstlisting} 54 | make 55 | make qemu 56 | \end{lstlisting} 57 | 58 | 则可以得到如下显示界面 59 | 60 | \begin{figure}[htbp] 61 | \centering 62 | \includegraphics{figures/3.5.2.1.png} 63 | \caption{3.5.2.1} 64 | \end{figure} 65 | 66 | 通过上图,我们可以看到ucore在切换到用户态之前,先显示了当前CPU的特权级(CS的最低两位),CS/DS/ES/SS的值(即对应的段描述符表的索引值),可以看到特权级为0。根据lgdt函数(位于kern/mm/pmm.c中)的处理,CS的值是内核代码段描述符的索引下标,DS/ES/SS的值是内核数据段描述符的索引下标;而在切换到用户态后,又显示了一下,当前CPU的特权级为3, 67 | CS的值为1b,DS/ES/SS的值为23,把这四个寄存器的值\&0xfc,则分别为0x18(SEG\_UTEXT)和0x20(SEG\_UDATA),说明确实运行在用户态了。在执行了系统调用T\_SWITCH\_TOK后,又回到了内核态。下面我们将分析到底发生了什么事情。 68 | -------------------------------------------------------------------------------- /latex/proj4_intr_in_ucore.tex: -------------------------------------------------------------------------------- 1 | \section{可管理中断并处理中断方式I/O的ucore}\label{ux53efux7ba1ux7406ux4e2dux65adux5e76ux5904ux7406ux4e2dux65adux65b9ux5f0fioux7684ucore} 2 | 3 | \subsection{实验目标}\label{ux5b9eux9a8cux76eeux6807} 4 | 5 | 前面的project都没有引入中断机制,所以bootloader和ucore都是正常地顺序执行,不会受到外界(比如外设)的``干扰''。虽然实现简单,但无法解决上述问题。我们需要扩展ucore的功能,让ucore能够支持中断,这需要读者了解基本的80386硬件中断机制,对保护模式有更深入的了解;需要清楚在中断的处理过程中,硬件主动完成了什么事情,软件在硬件完成的基础上又要完成哪些事情。通过学习和实践,读者可以了解清楚上述问题,并进一步知道通过操作系统的中断处理例程(Interrupt 6 | Process Routine, IPR)完成设备请求处理的方法等。 7 | 8 | \subsection{proj4概述}\label{proj4ux6982ux8ff0} 9 | 10 | \subsubsection{实现描述}\label{ux5b9eux73b0ux63cfux8ff0} 11 | 12 | proj4建立在proj3.1的基础上,实现了一个通过中断机制完成设备(键盘、串口和时钟)中断请求处理的ucore。简单地说proj4扩展与中断相关的工作有两个,一个是初始化中断,涉及初始化中断控制器8259A(打通外设与CPU的通路)和中断门描述符表(建立外设中断与中断服务例程的联系)和各种外设。以proj4的ucore为例,操作系统内核启动以后,kern\_init函数(kern/init/init.c)通过调用pic\_init函数完成对中断控制器的初始化工作,调用idt\_init函数完成了对整个中断门描述符表的创建,调用cons\_init和clock\_init函数完成对串口、键盘和时钟外设的中断初始化工作。 13 | 14 | ucore的另一个重要工作是中断服务,即收到中断后,对中断进行处理的中断服务例程(比如收到100个时钟中断后,显示一个字符串``100 15 | ticks'')等。这主要集中在vectors.S(包括256个中断服务例程的入口地址和第一步初步处理实现)、trapentry.S(紧接着第一步初步处理后,进一步完成第二步初步处理的实现以及中断处理完毕后的返回准备工作)和trap.c中(紧接着第二步初步处理后,继续完成具体的各种中断处理操作)。 16 | 17 | \subsubsection{项目组成}\label{ux9879ux76eeux7ec4ux6210} 18 | 19 | proj4整体目录结构如下所示: 20 | 21 | \begin{lstlisting} 22 | proj4 23 | |-- kern 24 | | |-- driver 25 | | | |-- clock.c 26 | | | |-- clock.h 27 | | | |-- console.c 28 | | | |-- console.h 29 | | | |-- picirq.c 30 | | | `-- picirq.h 31 | | |-- init 32 | | | `-- init.c 33 | | |-- mm 34 | | | |-- memlayout.h 35 | | | `-- mmu.h 36 | | `-- trap 37 | | |-- trap.c 38 | | |-- trapentry.S 39 | | |-- trap.h 40 | | `-- vectors.S 41 | `-- tools 42 | `-- vector.c 43 | …… 44 | \end{lstlisting} 45 | 46 | proj4是基于proj3.1(会在内置监控自身运行状态的ucore一节中进一步说明)进一步扩展完成的。相对于proj3.1,增加了大约10个文件,相关增加和改动主要集中在kern/driver和kern/trap目录下,使得ucore具有外设中断处理功能,这一个比较大的跨越。主要增加和修改的文件如下所示: 47 | 48 | \begin{itemize} 49 | \item 50 | tools/vector.c:生成vectors.S,此文件包含了中断向量处理的统一实现。 51 | \item 52 | kern/driver/intr.{[}ch{]}:实现了通过设置CPU的eflags来屏蔽和使能中断的函数; 53 | \item 54 | kern/driver/picirq.{[}ch{]}:实现了对中断控制器8259A的初始化和使能操作; 55 | \item 56 | kern/driver/clock.{[}ch{]}:实现了对时钟控制器8253的初始化操作; 57 | \item 58 | kern/driver/console.{[}ch{]}:实现了对串口和键盘的中断方式的处理操作; 59 | \item 60 | kern/trap/vectors.S:包括256个中断服务例程的入口地址和第一步初步处理实现; 61 | \item 62 | kern/trap/trapentry.S:紧接着第一步初步处理后,进一步完成第二步初步处理;并且有恢复中断上下文的处理,即中断处理完毕后的返回准备工作; 63 | \item 64 | kern/trap/trap.{[}ch{]}:紧接着第二步初步处理后,继续完成具体的各种中断处理操作; 65 | \end{itemize} 66 | 67 | \subsubsection{编译运行}\label{ux7f16ux8bd1ux8fd0ux884c} 68 | 69 | \textbf{编译运行} 70 | 71 | 编译并运行proj4的命令如下: 72 | 73 | \begin{lstlisting} 74 | make 75 | make qemu 76 | \end{lstlisting} 77 | 78 | 则可以得到如下显示界面 79 | 80 | 通过上图可以看到时钟中断已经能够正常相应,每隔100个时钟中断会显示一次``100 81 | ticks''的信息。一个简单的显示信息的背后蕴藏着中断处理的复杂实现。下面我们将从中断基本概念、中断控制器、保护模式的中断处理机制等方面来分析上图中背后的东西。 82 | -------------------------------------------------------------------------------- /latex/proj7_8_9.tex: -------------------------------------------------------------------------------- 1 | \section{proj7/8/9/9.1/9.2概述}\label{proj7899.19.2ux6982ux8ff0} 2 | 3 | 为了实现虚存管理,首先需要能够处理缺页异常,这是需要对当前的trap处理进行扩展,并能够描述当前内核中``合法''的虚拟内存(不一定有对应的物理内存)。proj7在proj6的基础上实现了上述过程,新增加的主要工作包括: 4 | 5 | \begin{itemize} 6 | \item 7 | 描述当前``合法''的虚拟内存的数据结构vma\_struct和针对vma\_struct的函数操作; 8 | \item 9 | 扩展trap\_dispatch函数,使得能够根据vma\_struct结构的描述,正确完成对缺页的处理(即如果发现是``合法''的虚拟内存地址,则创建或修改页表项来建立与物理内存页的对应关系)。 10 | \end{itemize} 11 | 12 | 为了提供超过物理内存大小的虚拟内存空间,需要把不常用的页换出到硬盘上,这样当访问到这些不存在的虚存页时,会产生缺页异常,可以把这些页再从硬盘拷贝回到内存中。proj8在proj7的基础上完成上述过程的实现,新增加的主要工作包括: 13 | 14 | \begin{itemize} 15 | \item 16 | 为了准备swap in/out,实现通过PIO方式读写IDE格式的硬盘; 17 | \item 18 | 建立swap相关数据结构和相关操作,确保不常用的页能够被换出(swap 19 | out)到硬盘上,并在被访问时,能够从硬盘对应的扇区中换入(swap 20 | in)到内存中; 21 | \end{itemize} 22 | 23 | 为了实现将来不同进程(用户态程序)之间共享内存,需要对描述虚拟内存的vma\_strct结构进行扩展。proj9/9.1在proj8的基础上完成上述过程的实现,新增加的主要工作包括: 24 | 25 | \begin{itemize} 26 | \item 27 | 增加shmem\_node结构的描述,确保能够描述多个虚拟页映射到一个物理页的情况,并增加针对shmem\_node的处理。 28 | \item 29 | 为了减少复制内存的开销,可通过实现写时复制(Copy On 30 | Write,简称COW)机制来完成,其基本思路是在只读情况下,多个虚拟页只需映射到一个物理页上,当对虚拟页进行写操作时,才真正完成对物理页的复制。在实现上需要对page的属性进行扩展,能够在发生页保护异常时,探测出是为了``写时复制''而设置的页,这样在缺页异常处理中,会完成实际的分配新页操作。proj9.2在proj9.1的基础上完成上述过程的实现,新增加的主要工作包括: 31 | \item 32 | 扩展trap\_dispatch函数,使得能够根据产生异常的地址的页表项内容和此地址对应的vma中的属性描述,正确完成对的``写时复制''处理。 33 | \end{itemize} 34 | -------------------------------------------------------------------------------- /latex/reference.tex: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /latex/rv_pages_hardware.tex: -------------------------------------------------------------------------------- 1 | % rv_pages_hardware 2 | -------------------------------------------------------------------------------- /latex/schedule.tex: -------------------------------------------------------------------------------- 1 | % schedule.tex 2 | \chapter{调度}\label{ch_sched} 3 | 4 | \section{本章概要} 5 | 6 | \paragraph{一句话描述} 7 | 8 | \paragraph{概述} 9 | 10 | \paragraph{本章涉及的实验} 11 | 12 | \paragraph{本章收获的知识} 13 | 14 | %\input{} 15 | %\input{} 16 | %\input{} 17 | %\input{} 18 | %\input{} 19 | %\input{} 20 | %\input{} 21 | 22 | 23 | \section{小结} 24 | 缺 -------------------------------------------------------------------------------- /latex/setup_stack.tex: -------------------------------------------------------------------------------- 1 | \section{【实现】设置栈}\label{ux5b9eux73b0ux8bbeux7f6eux6808} 2 | 3 | 只有设置好的合适大小和地址的栈内存空间(简称栈空间),才能有效地进行函数调用。这里为了减少汇编代码量,我们就通过C代码来完成显示。由于需要调用C语言的函数,所以需要自己建立好栈空间。设置栈的代码如下: 4 | 5 | \begin{lstlisting} 6 | movl $start, %esp 7 | \end{lstlisting} 8 | 9 | 由于start位置(0x7c00)前的地址空间没有用到,所以可以用来作为bootloader的栈,需要注意栈是向下长的,所以不会破坏start位置后面的代码。在后面的小节还会对栈进行更加深入的讲解。我们可以通过用gdb调试bootloader来进一步观察栈的变化: 10 | 11 | \textbf{【实验】用gdb调试bootloader观察栈信息 } 12 | 13 | \begin{enumerate} 14 | \def\labelenumi{\arabic{enumi}.} 15 | \item 16 | 开两个窗口;在一个窗口中,在proj1目录下执行命令make; 17 | \item 18 | 在proj1目录下执行 ``qemu -hda bin/ucore.img -S 19 | -s'',这时会启动一个qemu窗口界面,处于暂停状态,等待gdb链接; 20 | \item 21 | 在另外一个窗口中,在proj1目录下执行命令 gdb obj/bootblock.o; 22 | \item 23 | 在gdb的提示符下执行如下命令,会有一定的输出: 24 | 25 | \begin{lstlisting} 26 | (gdb) target remote :1234 #与qemu建立远程链接 27 | (gdb) break bootasm.S:68 #在bootasm.S的第68行“movl $start, %esp”设置一个断点 28 | (gdb) continue #让qemu继续执行 29 | \end{lstlisting} 30 | 31 | 这时qemu会继续执行,但执行到bootasm.S的第68行时会暂停,等待gdb的控制。这时可以在gdb中继续输入如下命令来分析栈的变化: 32 | 33 | \begin{lstlisting} 34 | (gdb) info registers esp 35 | esp 0xffd6 0xffd6 #没有执行第68行代码前的esp值 36 | (gdb) si #执行第68行代码 37 | 69 call bootmain 38 | (gdb) info registers esp 39 | esp 0x7c00 0x7c00 #当前的esp值,即栈顶 40 | (gdb) si 41 | bootmain () at boot/bootmain.c:87 #执行call汇编指令 42 | 87 bootmain(void) { 43 | (gdb) info registers esp 44 | esp 0x7bfc 0x7bfc #当前的esp值0x7bfc, 0x7bfc处存放了bootmain函数的返回地址0x7c4a,这可以通过下面两个命令了解 45 | (gdb) x /4x 0x7bfc 46 | 0x7bfc: 0x00007c4a 0xc031fcfa 0xc08ed88e 0x64e4d08e 47 | (gdb) x /4i 0x7c40 48 | 0x7c40 : mov $0x7c00,%esp 49 | 0x7c45 : call 0x7c6c 50 | 0x7c4a : jmp 0x7c4a 51 | 0x7c4c : add %al,(%eax) 52 | \end{lstlisting} 53 | \end{enumerate} 54 | 55 | \subsection{【提示】}\label{ux63d0ux793a} 56 | 57 | 在proj1中执行 58 | 59 | \begin{lstlisting} 60 | make debug 61 | \end{lstlisting} 62 | 63 | 则自动完成上述大部分前期工作,即qemu和gdb的加载,且gdb会自动建立于qemu的联接并设置好断点。具体实现可参看proj1的Makefile中于debug相关的内容和tools/gdbinit中的内容。 64 | -------------------------------------------------------------------------------- /latex/setuplinuxenv.tex: -------------------------------------------------------------------------------- 1 | \section{建立Linux实验环境}\label{setuplinux} 2 | 在网上找一个可以实践Linux的环境吧! -------------------------------------------------------------------------------- /latex/setupmammalcomputerenv.tex: -------------------------------------------------------------------------------- 1 | \section{建立mammal-computer实验环境}\label{setupmammalcomp} 2 | 找一个可以实践mammal-computer环境下的mammal-os的环境吧! -------------------------------------------------------------------------------- /latex/show_string.tex: -------------------------------------------------------------------------------- 1 | \section{【实现】显示字符串}\label{ux5b9eux73b0ux663eux793aux5b57ux7b26ux4e32} 2 | 3 | bootloader只在CPU和内存中打转无法让读者很容易知道bootloader的工作是否正常,为此在成功完成了保护模式的转换后,就需要通过显示字符串来展示一下自己了。bootloader设置好栈后,就可以调用bootmain函数显示字符串了。在proj1中使用了显示器和并口两种外设来显示字符串,主要的代码集中在bootmain.c中。 4 | 5 | 这里采用的是很简单的基于Programmed I/O 6 | (PIO)方式,PIO方式是一种通过CPU执行I/O端口指令来进行数据读写的数据交换模式,被广泛应用于硬盘、光驱等设备的基础传输模式中。这种I/O访问方式使用CPU 7 | I/O端口指令来传送所有的命令、状态和数据,需要CPU全程参与,效率较低,但编程很简单。后面讲到的中断方式将更加高效。 8 | 在bootmain.c中的lpt\_putc函数完成了并口输出字符的工作。输出一个字符的流程(可参看bootmain.c中的lpc\_putc函数实现)大致如下: 9 | 10 | \begin{enumerate} 11 | \def\labelenumi{\arabic{enumi}.} 12 | \item 13 | 读I/O端口地址0x379,等待并口准备好; 14 | \item 15 | 向I/O端口地址0x378发出要输出的字符; 16 | \item 17 | 向I/O端口地址0x37A发出控制命令,让并口处理要输出的字符。 18 | \end{enumerate} 19 | 20 | 在bootmain.c中的serial\_putc函数完成了串口输出字符的工作。输出一个字符的流程(可参看bootmain.c中的serial\_putc函数实现)大致如下: 21 | 22 | \begin{enumerate} 23 | \def\labelenumi{\arabic{enumi}.} 24 | \item 25 | 读I/O端口地址(0x3f8+5)获得LSR寄存器的值,等待串口输出准备好; 26 | \item 27 | 向I/O端口地址0x3f8发出要输出的字符; 28 | \end{enumerate} 29 | 30 | 在bootmain.c中的cga\_putc函数完成了CGA字符方式在某位置输出字符的工作。输出一个字符的流程(可参看bootmain.c中的cga\_putc函数实现)大致如下: 31 | 32 | \begin{enumerate} 33 | \def\labelenumi{\arabic{enumi}.} 34 | \item 35 | 写I/O端口地址0x3d4,读I/O端口地址0x3d5,获得当前光标位置; 36 | \item 37 | 在光标的下一位置的显存地址空间上写字符,格式是黑色背景/白色字符; 38 | \item 39 | 设置当前光标位置为下一位置。 40 | \end{enumerate} 41 | 42 | proj1启动后的PC机内存布局如下图所示: 43 | 44 | %\begin{figure}[htbp] 45 | %\centering 46 | %\includegraphics{figures/3.18.1.png} 47 | %\caption{3.18.1} 48 | %\end{figure} 49 | 50 | 自此,我们了解了一个小巧的bootloader的实现过程,但这还仅仅是百尺竿头的第一步,它还只能显示字符串,不能加载操作系统。我们还需要扩展bootloader的功能,让它能够加载操作系统。 51 | -------------------------------------------------------------------------------- /latex/show_string_in_ucore.tex: -------------------------------------------------------------------------------- 1 | \section{【实现】可输出字符串的ucore}\label{ux5b9eux73b0ux53efux8f93ux51faux5b57ux7b26ux4e32ux7684ucore} 2 | 3 | proj3包含了一个只能输出字符串的简单ucore操作系统,虽然简单,但它也体现了操作系统的一些结构和特征,比如它具有: 4 | 5 | \begin{itemize} 6 | \item 7 | 完成给ucore的BSS段清零并显示一个字符串的内核初始化子系统(init.c) 8 | \item 9 | 提供串口/并口/CGA显示的驱动程序子系统(console.c) 10 | \item 11 | 提供公共服务的操作系统函数库子系统(printf.c printfmt.c string.c) 12 | \end{itemize} 13 | 14 | 这体现了操作系统的一个基本特征:资源管理器。从操作系统原理我们可以知道一台计算机就是一组资源,这些资源用于对数据的移动、存储和处理并进行控制。在proj3中的ucore操作系统目前只提供了对串口/并口/CGA这三种I/O设备的硬件资源的访问,每个I/O设备的操作都有自己特有的指令集或控制信号(对照一下serial\_putc/lpt\_putc/cga\_putc函数的实现),操作系统隐藏这些细节,并提供了统一的接口(看看cprintf函数的实现),因此程序员可以使用简单的printf函数来写这些设备,达到显示数据的效果。目前操作系统的逻辑结构图架构如下图所示: 15 | 16 | \begin{figure}[htbp] 17 | \centering 18 | \includegraphics{figures/3.2.7.1.png} 19 | \caption{3.2.7.1} 20 | \end{figure} 21 | 22 | 在PC中的地址空间布局图如下所示: 23 | 24 | \begin{figure}[htbp] 25 | \centering 26 | \includegraphics{figures/3.2.7.2.png} 27 | \caption{3.2.7.2} 28 | \end{figure} 29 | -------------------------------------------------------------------------------- /latex/slab.tex: -------------------------------------------------------------------------------- 1 | \section{【实现】slab算法的简化设计实现}\label{ux5b9eux73b0slabux7b97ux6cd5ux7684ux7b80ux5316ux8bbeux8ba1ux5b9eux73b0} 2 | 3 | \subsection{数据结构描述}\label{ux6570ux636eux7ed3ux6784ux63cfux8ff0} 4 | 5 | slab 算法采用了两层数据组织结构。在最高层是 6 | slab\_cache,这是一个不同大小slab 7 | 缓存的链接列表数组。slab\_cache的每个数组元素都是一个管理和存储给定大小的空闲对象(obj)的slab 8 | 结构链表,这样每个slab设定一个要管理的给定大小的对象池,占用物理空间连续的1个或多个物理页。slab\_cache的每个数组元素管理两种slab列表: 9 | 10 | \begin{itemize} 11 | \item 12 | slabs\_full:完全分配的 slab 13 | \item 14 | slabs\_notfull:部分分配的 slab 15 | \end{itemize} 16 | 17 | 注意 slabs\_notfull列表中的 slab 是可以进行回收(reaping),使得slab 18 | 所使用的内存可被返回给操作系统供其他子系统使用。 19 | 20 | slab 列表中的每个 slab 21 | 都是一个连续的内存块(一个或多个连续页),它们被划分成一个个obj。这些obj是中进行分配和释放的基本元素。由于对象是从 22 | slab 中进行分配和释放的,因此单个 slab 可以在 slab 23 | 链表之间进行移动。例如,当一个 slab 中的所有对象都被使用完时,就从 24 | slabs\_notfull 链表中移动到 slabs\_full 链表中。当一个 slab 25 | 完全被分配并且有对象被释放后,就从 slabs\_full 列表中移动到 26 | slabs\_notfull列表中。下面是ucore中的slab架构图: 27 | 28 | \begin{figure}[htbp] 29 | \centering 30 | \includegraphics{figures/9.png} 31 | \caption{8} 32 | \end{figure} 33 | 34 | slab架构图 35 | 36 | \subsection{分配与释放内存实现描述}\label{ux5206ux914dux4e0eux91caux653eux5185ux5b58ux5b9eux73b0ux63cfux8ff0} 37 | 38 | 现在来看一下能够创建新 slab 缓存、向缓存中增加内存、销毁缓存的接口以及 39 | slab 中对对象进行分配和释放操作的slab相关操作过程和函数。 40 | 41 | 第一个步骤是通过执行slab\_init函数初始化slab\_cache 缓存结构。然后其他 42 | slab 43 | 缓存函数将使用该引用进行内存分配与释放等操作。ucore中最常用的内存管理函数是 44 | kmalloc 和 kfree 函数。这两个函数的原型如下: 45 | 46 | \begin{itemize} 47 | \item 48 | void *kmalloc( size\_t size ); 49 | \item 50 | void kfree(void *objp ); 51 | \end{itemize} 52 | 53 | 在分配任意大小的空闲块时,kmalloc通过调用kmem\_cache\_alloc函数来遍历对应size的slab,来查找可以满足大小限制的缓存。如果kmem\_cache\_alloc函数发现slab中有空闲的obj,则分配这个对象;如果没有空闲的obj了,则调用kmem\_cache\_grow函数分配包含1到多页组成的空闲slab,然后继续分配。要使用 54 | kfree 55 | 释放对象时,通过进一步调用kmem\_cache\_free把分配对象返回到slab中,并标记为空闲,建立空闲obj之间的链接关系。 56 | 57 | \lstinline!(可进一步详细一些)! 58 | -------------------------------------------------------------------------------- /latex/sosbook.bib: -------------------------------------------------------------------------------- 1 | % -*- coding: utf-8 -*- 2 | 3 | @book{csao2015, 4 | title={Computer science : an overview.}, 5 | edition = {12th}, 6 | author={Brookshear, Brylow}, 7 | year={2015}, 8 | publisher={Princeton University Press}, 9 | address={Princeton}, 10 | } 11 | 12 | @book{mwi2004, 13 | author = {Mark E. Russinovich and David A. Solomon}, 14 | edition = {4th}, 15 | publisher = {Microsoft Press}, 16 | title = {Microsoft Windows Internals: Microsoft Windows Server 17 | 2003, Windows XP, and Windows 2000}, 18 | year = {2004}, 19 | } 20 | 21 | @inproceedings{lottery1994, 22 | author = {Carl A. Waldspurger and William E. Weihl}, 23 | booktitle = {Proceedings of the First Symposium on Operating 24 | System Design and Implementation (OSDI)}, 25 | pages = {1-11}, 26 | title = {Lottery Scheduling: Flexible Proportional-Share 27 | Resource Management}, 28 | year = {1994}, 29 | } 30 | 31 | @article{scalesync1991, 32 | address = {New York, NY, USA}, 33 | author = {Mellor-Crummey, John M. and Scott, Michael L.}, 34 | journal = tocs, 35 | month = {February}, 36 | pages = {21-65}, 37 | publisher = {ACM}, 38 | title = {Algorithms for scalable synchronization on 39 | shared-memory multiprocessors}, 40 | volume = {9}, 41 | year = {1991}, 42 | doi = {http://doi.acm.org/10.1145/103727.103729}, 43 | issn = {0734-2071}, 44 | url = {http://doi.acm.org/10.1145/103727.103729}, 45 | } 46 | 47 | @article{Abernathy:1974:SDG:775265.775269, 48 | author = {Abernathy, David H. and Mancino, John S. and Pearson, Charls R. and Swiger, Dona C.}, 49 | title = {Survey of Design Goals for Operating Systems}, 50 | journal = {SIGOPS Oper. Syst. Rev.}, 51 | issue_date = {January 1974}, 52 | volume = {8}, 53 | number = {1}, 54 | month = jan, 55 | year = {1974}, 56 | issn = {0163-5980}, 57 | pages = {25--35}, 58 | numpages = {11}, 59 | url = {http://doi.acm.org/10.1145/775265.775269}, 60 | doi = {10.1145/775265.775269}, 61 | acmid = {775269}, 62 | publisher = {ACM}, 63 | address = {New York, NY, USA}, 64 | } 65 | -------------------------------------------------------------------------------- /latex/stack_process.tex: -------------------------------------------------------------------------------- 1 | \section{【背景】栈结构和处理过程}\label{ux80ccux666fux6808ux7ed3ux6784ux548cux5904ux7406ux8fc7ux7a0b} 2 | 3 | 根据数据结构课程中的描述,栈是限定仅在表尾进行插入(即入栈操作)或删除操作(即出栈操作)的线性表{[}严蔚敏著的《数据结构》书{]}。因此,栈的表尾端成为称为栈顶(stack 4 | top),表头端称为栈底(stack 5 | bottom)。在X86CPU架构中有专门的指令``push''来完成入栈操作,``pop''指令来完成出栈操作,栈顶指针寄存器ESP时刻指向栈的栈顶。比较有趣的是,在x86中,采用的是满降序栈(full 6 | descending 7 | stack)机制,即栈底在高地址,栈顶在低地址,入栈的方向是向低地址进行,出栈的方向是向高地址进行,栈指针指向上次写的最后一个数据单元。GCC编译器规定的函数栈帧(stack 8 | frame)是一块存放某函数的局部变量、参数、返回地址和其它临时变量的内存空间,栈帧的大致结构和操作如下图所示: 9 | 10 | \begin{figure}[htbp] 11 | \centering 12 | \includegraphics{figures/3.3.3.1.png} 13 | \caption{3.3.3.1} 14 | \end{figure} 15 | 16 | 操作系统中使用栈的目的与一般应用程序类似,不外乎包括:支持函数调用、传递函数参数、局部变量(也称自动变量)存储、函数返回值存储、在函数内部保存可能被修改的寄存器的值供恢复。除此之外,在后续讲到的中断处理、内核态/用户态切换、进程切换等方面,也需要使用栈来保存被打断的执行所用到的寄存器等硬件信息。由于在操作系统中要完成许多非常规的栈空间数据处理,比如直接修改保存在栈帧中的返回地址,使得函数返回到不同的地方去,所以我们需要在计算机体系结构和机器代码级别更加深入地理解操作系统是如何具体进行栈处理的。下面,我们将结合调试运行proj3.1来分析内核中的函数调用关系。 17 | -------------------------------------------------------------------------------- /latex/startingos.tex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/latex/startingos.tex -------------------------------------------------------------------------------- /latex/summary.tex: -------------------------------------------------------------------------------- 1 | \section{原理归纳与小结}\label{ux539fux7406ux5f52ux7eb3ux4e0eux5c0fux7ed3} 2 | 3 | 读者通过阅读本章和做实验,相信已经尝试了加载操作系统、操作系统访问外设、操作系统处理中断、用户态与内核态切换等一系列小project。这里面体现了哪些操作系统的原理和概念呢? 4 | 5 | 系统软件:在这里读者应该体会到操作系统是一个软件,采用编译器生成可执行代码,通过bootloader加载到内存中执行,可以执行所有CPU指令(包括特权指令),访问到所有硬件资源。相对于一般的应用软件,它主要完成的工作是对计算机的硬件资源管理,并给上层应用提供服务(这里还体现不够)。所以我们把操作系统看成是提供计算机系统级管理和基础共性服务的一种系统软件。 6 | 7 | 段式的内存管理:在这里读者应该体会到CPU访问的地址首先是一个虚拟地址,然后通过MMU的段式地址保护检查和变换(这要看段描述符表中的描述符是如何设置段基址和段范围的)才能把虚拟地址变换成线性地址(由于没有启动分页机制,线性地址就是物理地址),如果虚拟地址的偏移值超过了段范围值,这会出现故障中断。 8 | 9 | 中断处理:在这里读者应该体会到中断包含了外设产生的外设中断(来的时机不确定)和CPU主动产生的陷阱中断(执行特定指令就会产生),有了中断,操作系统就可以随时打断CPU正常的顺序执行,转而处理相对更加紧急的中断事件。为了能够让CPU继续正常执行,在中断处理前需要保存足够的硬件信息(主要是CPU各种寄存器的值,一般放在内核栈中),以便于中断处理完毕后,通过恢复这些硬件信息,能够回到被打断的地方继续执行。 10 | 11 | 特权级:在这里读者应该体会到在不同的特权级可以完成的事情是不一样的。操作系统需要管理整个计算机资源,所以它应该运行在CPU的最高特权级,而应用程序不必也不能使用特权指令或访问操作系统的地址空间,所以它应该运行在CPU的非特权级。如果应用程序执行特权指令或访问操作系统的的地址空间,则CPU硬件的安全检查机制会阻止应用程序的执行并尝试故障中断,把CPU控制权转给操作系统进行进一步处理。如果应用程序需要访问计算机资源,可以通过执行特定的指令,产生陷阱中断,从而完成从用户态到内核态的切换,让操作系统为应用程序提供服务。 12 | -------------------------------------------------------------------------------- /latex/support_virtual_mem_managment.tex: -------------------------------------------------------------------------------- 1 | \section{实现虚存管理功能}\label{ux5b9eux73b0ux865aux5b58ux7ba1ux7406ux529fux80fd} 2 | 3 | \subsection{试验目标}\label{ux8bd5ux9a8cux76eeux6807} 4 | 5 | 有了页表的支持,我们可以使得不同用户态运行程序的内存空间之间无法访问,达到隔离和保护的作用。但页表如何仅仅只支持这个功能就太大材小用了。我们其实还可以通过页表实现更多的功能: 6 | 7 | \begin{itemize} 8 | \item 9 | 内存共享:把两个虚拟地址空间通过页表映射到同一物理地址空间。这只需通过设置不同索引的页表项的内容一致即可。 10 | \item 11 | 提供超过物理内存大小的虚拟内存空间:这一步需要结合异常中断处理和硬盘来完成。其基本思想是在内存中放置最常用的一些数据,不常用的数据会被放到硬盘上,但给用户态的软件一种感觉,觉得这些数据都在内存中。当用户态软件访问到的数据不在内存中(暂时存放在硬盘上)的时候,这条访存指令会引发异常中断,由操作系统的异常中断处理例程进行管理。这时操作系统会分析引发异常的内存地址,能够把对应缓存在硬盘中的数据重新读入这个内存地址,并让用户态软件重新执行产生访存异常的那条指令。这些由操作系统完成的工作在用户态完全``看''不到。从用户态软件的角度看,只是操作系统给用户提供了一个超出实际物理内存大小的虚拟内存空间。 12 | \item 13 | 按需分配内存:用户态软件在运行时要求操作系统提供很大的内存,操作系统``表面上''表示满足用户需求,但在背后并没有实际分配对应的物理内存空间。等到用户态软件实际执行到对这些内存的访问时,由于没有分配对应的物理内存空间,会导致产生访存异常。操作系统的异常中断处理例程发觉这是用户态软件以前确实要求过的内存空间,则在从系统管理的空闲空间中分配一页或几页物理内存给用户态软件,并让用户态软件重新执行产生访存异常的那条指令。这些由操作系统完成的工作在用户态也完全``看''不到。但从操作系统的整体管理的角度看,这种方式在用户态软件确实需要的时候把内存分配给用户态软件,提高了内存的使用率,避免了用户态软件``圈地不用''的现象。 14 | \end{itemize} 15 | 16 | 为了高效地完成上述三件事情,操作系统需要考虑应该把哪些不常用的内存换出到硬盘上去,这就是内存的页替换算法,常见的有LRU算法,Clock算法,二次机会法等。而在实现上,由于涉及异常处理和硬盘管理等,虚存管理在整个ucore实现中的相对复杂度是最大的。 17 | -------------------------------------------------------------------------------- /latex/syncmutex.tex: -------------------------------------------------------------------------------- 1 | % syncmutex.tex 2 | \chapter{同步互斥}\label{ch_syncmutex} 3 | 4 | \section{本章概要} 5 | 6 | \paragraph{一句话描述} 7 | 8 | \paragraph{概述} 9 | 10 | \paragraph{本章涉及的实验} 11 | 12 | \paragraph{本章收获的知识} 13 | 14 | %\input{} 15 | %\input{} 16 | %\input{} 17 | %\input{} 18 | %\input{} 19 | %\input{} 20 | %\input{} 21 | 22 | 23 | \section{小结} 24 | 缺 -------------------------------------------------------------------------------- /latex/task_switch.tex: -------------------------------------------------------------------------------- 1 | \section{【背景】80386的任务切换}\label{ux80ccux666f80386ux7684ux4efbux52a1ux5207ux6362} 2 | 3 | \lstinline!任务!是80386硬件描述中的一个名词,在这里我们可以简单地把运行在内核态的ucore成为一个任务,把运行在用户态的应用称为另外一任务。任务寄存器(Task 4 | Register,简称TR) 5 | 储存了一个16位的选择子(对软件可见),用来索引全局描述符表(GDT)中的一项。TR对应的描述符描述的一个任务状态段(TSS:Task 6 | Status Segment)。 7 | 8 | TSS 任务状态段(Task State 9 | Segment,简称TSS)。任务状态段(TSS)是位于GDT中的一个系统段描述符。任务状态段是做什么的呢?任务状态段就是内存中的一个数据结构。这个结构中保存着和任务相关的信息。当发生任务切换的时候会把当前任务用到的寄存器内容(CS/ 10 | EIP/ 11 | DS/SS/EFLAGS\ldots{})等保存在TSS中以便任务切换回来时候继续使用。ucore根据80386硬件手册建立的TSS数据结构如下所示: 12 | 13 | \begin{lstlisting} 14 | struct taskstate { 15 | uint32_t ts_link; // 链接字段 16 | uintptr_t ts_esp0; // 0级栈指针 17 | uint16_t ts_ss0; // 0级栈段寄存器 18 | uint16_t ts_padding1; 19 | uintptr_t ts_esp1; 20 | uint16_t ts_ss1; 21 | uint16_t ts_padding2; 22 | uintptr_t ts_esp2; 23 | uint16_t ts_ss2; 24 | uint16_t ts_padding3; 25 | physaddr_t ts_cr3; // 页目录基址寄存器 26 | uintptr_t ts_eip; // 切换的上次EIP 27 | uint32_t ts_eflags; 28 | uint32_t ts_eax; // 保存的通用寄存器eax 29 | uint32_t ts_ecx; 30 | uint32_t ts_edx; 31 | uint32_t ts_ebx; 32 | uintptr_t ts_esp; 33 | uintptr_t ts_ebp; 34 | uint32_t ts_esi; 35 | uint32_t ts_edi; 36 | uint16_t ts_es; // 保存的段寄存器 37 | uint16_t ts_padding4; 38 | uint16_t ts_cs; 39 | uint16_t ts_padding5; 40 | uint16_t ts_ss; 41 | uint16_t ts_padding6; 42 | uint16_t ts_ds; 43 | uint16_t ts_padding7; 44 | uint16_t ts_fs; 45 | uint16_t ts_padding8; 46 | uint16_t ts_gs; 47 | uint16_t ts_padding9; 48 | uint16_t ts_ldt; 49 | uint16_t ts_padding10; 50 | uint16_t ts_t; // 调试陷阱标志(只用位0) 51 | uint16_t ts_iomb; // i/o map 基地址 52 | }; 53 | \end{lstlisting} 54 | 55 | 从上图中可以 56 | ,TSS的基本格式由104字节组成。这104字节的基本格式是不可改变的,但在此之外系统软件还可定义若干附加信息。基本的104字节可分为链接字段区域、内层栈指针区域、地址映射寄存器区域、寄存器保存区域和其它字段等五个区域。 57 | 58 | 其中比较重要的是内层栈指针区域,为了有效地实现保护,同一个任务在不同的特权级下使用不同的栈。例如,当从外层特权级3变换到内层特权级0时,任务使用的栈也同时从3级变换到0级栈;当从内层特权级0变换到外层特权级3时,任务使用的栈也同时从0级栈变换到3级栈。所以ucore使用的是0级栈,用户态应用使用的是3级栈。 59 | TSS的内层栈指针区域中有三个栈指针,它们都是48位的全指针(16位的选择子和32位的偏移),分别指向0级、1级和2级栈的栈顶,依次存放在TSS中偏移为4、12及20开始的位置。当发生从3级向0级转移时,把0级栈指针装入0级的SS及ESP寄存器以变换到0级栈。没有指向3级栈的指针,因为3级是最外层,所以任何一个向内层的转移都不可能转移到3级。但是,当特权级由0级向3级变换时,并不把0级栈的指针保存到TSS的栈指针区域。这表明向3级向0级转移时,总是把0级栈认为是一个空栈。 60 | 61 | 当发生任务切换时,80386中各寄存器的当前值被自动保存到TR所指定的TSS中,然后下一任务的TSS的选择子被装入TR;最后,从TR所指定的TSS中取出各寄存器的值送到处理器的各寄存器中。由此可见,通过在TSS中保存任务现场各寄存器状态的完整映象,实现任务的切换。 62 | -------------------------------------------------------------------------------- /latex/ucore.tex: -------------------------------------------------------------------------------- 1 | \section{“麻雀“OS--uCore} 2 | 3 | 为了学习OS,需要了解一个上百万代码的操作系统吗?自己写一个操作系统难吗?别被现在上百万行的Linux和Windows操作系统吓倒。当年Thompson乘他老婆带着小孩度假留他一人在家时,写了UNIX;当年Linus还是一个21岁大学生时完成了Linux雏形。站在这些巨人的肩膀上,我们能否也尝试一下做“巨人”的滋味呢? 4 | 5 | MIT的Frans Kaashoek等在2006年参考PDP-11上的UNIX Version 6写了一个可在X86上跑的操作系统xv6(基于MIT License),用于学生学习操作系统。我们可以站在他们的肩膀上,基于xv6的设计,尝试着一步一步完成一个从“空空如也”到“五脏俱全”的“麻雀”操作系统—ucore,此“麻雀”包含虚存管理、进程管理、处理器调度、同步互斥、进程间通信、文件系统等主要内核功能,总的内核代码量(C+asm)不会超过5K行。充分体现了“小而全”的指导思想。 6 | 7 | ucore的运行环境可以是真实的计算机系统,目前支持运行在X86,MIPS,ARM,RISC-V等计算机系统中。不过考虑到调试和开发的方便,我们可采用硬件模拟器,比如QEMU、BOCHS、VirtualBox、VMware Player等。ucore的开发环境主要是GCC中的gcc、gas、ld和MAKE等工具,也可采用集成了这些工具的IDE开发环境Eclipse-CDT。运行环境和开发环境既可以在Linux或Windows中使用。 8 | 9 | 那我们准备如何一步一步实现ucore呢?安装一个操作系统的开发过程,我们可以有如下的开发步骤: 10 | 11 | \begin{enumerate} 12 | \def\labelenumi{\arabic{enumi}.} 13 | \item 14 | bootloader+toy 15 | ucore:理解操作系统启动前的硬件状态和要做的准备工作,了解运行操作系统的外设硬件支持,操作系统如何加载到内存中,理解两类中断--``外设中断'',``陷阱中断'',内核态和用户态的区别; 16 | \item 17 | 物理内存管理:理解x86分段/分页模式,了解操作系统如何管理物理内存; 18 | \item 19 | 虚拟内存管理:理解OS虚存的基本原理和目标,以及如何结合页表+中断处理(缺页故障处理)来实现虚存的目标,如何实现基于页的内存替换算法和替换过程; 20 | \item 21 | 内核线程管理:理解内核线程创建、执行、切换和结束的动态管理过程,以及内核线程的运行周期等; 22 | \item 23 | 用户进程管理:理解用户进程创建、执行、切换和结束的动态管理过程,以及在用户态通过系统调用得到内核中各种服务的过程; 24 | \item 25 | 处理器调度:理解操作系统的调度过程和调度算法; 26 | \item 27 | 同步互斥与进程间通信:理解同步互斥的具体实现以及对系统性能的影响,研究死锁产生的原因,如何避免死锁,以及线程/进程间如何进行信息交换和共享; 28 | \item 29 | 文件系统:理解文件系统的具体实现,与进程管理和内存管理等的关系,缓存对操作系统IO访问的性能改进,虚拟文件系统(VFS)、buffer~cache和disk~driver之间的关系。 30 | \end{enumerate} 31 | 32 | 其中每个开发步骤都是建立在上一个步骤之上的,就像搭积木,从一个一个小木块,最终搭出来一个小房子。在搭房子的过程中,完成从理解操作系统原理到实践操作系统设计与实现的探索过程。 33 | 34 | %这个房子最终的建筑架构和建设进度如下图所示 35 | %\textgreater{} (!可进一步标注处各个proj在下图中的位置) 36 | %\begin{figure}[htbp] 37 | % \centering 38 | % \includegraphics{figures/ucore_arch.png} 39 | % \caption{ucore操作系统架构} 40 | %\end{figure} -------------------------------------------------------------------------------- /latex/ucore_code.tex: -------------------------------------------------------------------------------- 1 | \section{【背景】操作系统执行代码的组成}\label{ux80ccux666fux64cdux4f5cux7cfbux7edfux6267ux884cux4ee3ux7801ux7684ux7ec4ux6210} 2 | 3 | ucore通过gcc编译和ld链接,形成了ELF格式执行文件kernel(位于bin目录下),这样kernel的内部组成与一般的应用程序差别不大。一般而言,一个执行程序的内容是至少由 4 | bss段、data段、text段三大部分组成。 * BSS段:BSS(Block Started by 5 | Symbol)段通常是指用来存放执行程序中未初始化的全局变量的一块存储区域。BSS段属于静态内存分配的存储空间。 6 | * 数据段:数据段(Data 7 | Segment)通常是指用来存放执行程序中已初始化的全局变量的一块存储区域。数据段属于静态内存分配的存储空间。 8 | * 代码段:代码段(Code Segment/Text 9 | Segment)通常是指用来存放程序执行代码的一块存储区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 10 | 某些CPU架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。 11 | 12 | ucore和一般应用程序一样,首先是保存在像硬盘这样的非易失性存储介质上,当需要运行时,被加载到内存中。这时,需要把代码段、数据段的内容拷贝到内存中。对于位于BSS段中的未初始化的全局变量,执行程序一般认为其值为零。所以需要把BSS段对应的内存空间清零,确保执行代码的正确运行。可查看init文件中的kern\_init函数的第一个执行语句``memset(edata, 13 | 0, end - edata);''。 14 | 15 | 随着ucore的执行,可能需要进行函数调用,这就需要用到栈(stack);如果需要动态申请内存,这就需要用到堆(heap)。堆和栈是在操作系统执行过程中动态产生和变化的,并不存在于表示内核的执行文件中。栈又称堆栈, 16 | 是用户存放程序临时创建的局部变量,即函数中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用函数的栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进后出特点,所以栈特别方便用来保存/恢复调用现场。可以把栈看成一个寄存、交换临时数据的内存区。堆是用于存放运行中被动态分配的内存空间,它的大小并不固定,可动态扩张或缩减,这需要操作系统自己进行有效的管理。 17 | -------------------------------------------------------------------------------- /latex/ucore_control_computer.tex: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /latex/user_to_kernel.tex: -------------------------------------------------------------------------------- 1 | \section{【实现】用户态切换到内核态}\label{ux5b9eux73b0ux7528ux6237ux6001ux5207ux6362ux5230ux5185ux6838ux6001} 2 | 3 | CPU在用户态执行到switch\_to\_kernel()函数(即执行``int 4 | T\_SWITCH\_TOK'')时,由于当前处于用户态,而中断产生后,CPU会进入内核态,所以存在特权级转换。硬件会在内核栈中压入Error 5 | Code(可选)、EIP、CS和EFLAGS、ESP(用户态特权级的)、SS(用户态特权级的)(如下图所示),然后跳转到到IDT中记录的中断号T\_SWITCH\_TOK所对应的中断服务例程入口地址处继续执行。通过2.3.7小节``中断处理过程''可知,会执行到trap\_disptach函数(位于trap.c): 6 | 7 | \begin{lstlisting} 8 | case T_SWITCH_TOK: 9 | if (tf->tf_cs != KERNEL_CS) { 10 | //发出中断时,CPU处于用户态,我们希望处理完此中断后,CPU继续在内核态运行, 11 | //所以把tf->tf_cs和tf->tf_ds都设置为内核代码段和内核数据段 12 | tf->tf_cs = KERNEL_CS; 13 | tf->tf_ds = tf->tf_es = KERNEL_DS; 14 | //设置EFLAGS,让用户态不能执行in/out指令 15 | tf->tf_eflags &= ~(3 << 12); 16 | 17 | switchu2k = (struct trapframe *)(tf->tf_esp - (sizeof(struct trapframe) - 8)); 18 | //设置临时栈,指向switchu2k,这样iret返回时,CPU会从switchu2k恢复数据, 19 | //而不是从现有栈恢复数据。 20 | memmove(switchu2k, tf, sizeof(struct trapframe) - 8); 21 | *((uint32_t *)tf - 1) = (uint32_t)switchu2k; 22 | } 23 | break; 24 | \end{lstlisting} 25 | 26 | 这样在trap将会返回,在\_\_trapret:中,根据switchk2u的内容完成对返回前的寄存器和栈的回复准备工作,最后通过iret指令,CPU返回``int 27 | T\_SWITCH\_TOU''的后一条指令处,以内核态模式继续执行。 28 | -------------------------------------------------------------------------------- /latex/virtual_mem_managment.tex: -------------------------------------------------------------------------------- 1 | \section{【原理】虚拟内存管理}\label{ux539fux7406ux865aux62dfux5185ux5b58ux7ba1ux7406} 2 | 3 | 什么是虚拟内存?简单地说,是指程序员或CPU 4 | ``需要''和直接``看到''的内存,这其实暗示了两点:1、虚拟内存单元不一定有实际的物理内存单元对应,即实际的物理内存单元可能不存在;2、如果虚拟内存单元对应有实际的物理内存单元,那二者的地址一般不是相等的。通过操作系统的某种内存管理和映射技术可建立虚拟内存与实际的物理内存的对应关系,使得程序员或CPU访问的虚拟内存地址会转换为另外一个物理内存地址。 5 | 6 | 那么这个``虚拟''的作用或意义在哪里体现呢?在操作系统中,虚拟内存其实包含多个虚拟层次,在不同的层次体现了不同的作用。首先,在有了分段或分页机制后,程序员或CPU直接``看到''的地址已经不是实际的物理地址了,这已经有一层虚拟化,我们可简称为内存地址虚拟化。有了内存地址虚拟化,我们就可以通过设置段界限或页表项来设定软件运行时的访问空间,确保软件运行不越界,完成内存访问保护的功能。 7 | 8 | 通过内存地址虚拟化,可以使得软件在没有访问某虚拟内存地址时不分配具体的物理内存,而只有在实际访问某虚拟内存地址时,操作系统再动态地分配物理内存,建立虚拟内存到物理内存的页映射关系,这种技术属于lazy 9 | load技术,简称按需分页(demand 10 | paging)。把不经常访问的数据所占的内存空间临时写到硬盘上,这样可以腾出更多的空闲内存空间给经常访问的数据;当CPU访问到不经常访问的数据时,再把这些数据从硬盘读入到内存中,这种技术称为页换入换出(page 11 | swap 12 | in/out)。两个虚拟页的数据内容相同时,可只分配一个物理页框,这样如果对两个虚拟页的访问方式是只读方式,这这两个虚拟页可共享页框,节省内存空间;如果CPU对其中之一的虚拟页进行写操作,则这两个虚拟页的数据内容会不同,需要分配一个新的物理页框,并将物理页框标记为可写,这样两个虚拟页面将映射到不同的物理页帧,确保整个内存空间的正确访问。这种技术称为写时复制(Copy 13 | On 14 | Write,简称COW)。这三种内存管理技术给了程序员更大的内存``空间'',我们称为内存空间虚拟化。 15 | 16 | ucore在实现上述三种技术时,需要解决的一个关键问题是,何时进行请求调页/页换入换出/写时复制处理?其实,在程序的执行过程中由于某种原因(页框不存在/写只读页等)而使 17 | CPU 18 | 无法最终访问到相应的物理内存单元,即无法完成从虚拟地址到物理地址映射时,CPU 19 | 会产生一次缺页异常,从而需要进行相应的缺页异常服务例程。这个缺页异常处理的时机就是求调页/页换入换出/写时复制处理的执行时机,当相关处理完成后,缺页异常服务例程会返回到产生异常的指令处重新执行,使得软件可以继续正常运行下去。 20 | -------------------------------------------------------------------------------- /latex/vma_struct.tex: -------------------------------------------------------------------------------- 1 | \section{【实现】vma\_struct数据结构和相关操作}\label{ux5b9eux73b0vmaux5fstructux6570ux636eux7ed3ux6784ux548cux76f8ux5173ux64cdux4f5c} 2 | 3 | 在讲述缺页异常处理前,需要建立好虚拟内存空间描述。在proj7之前有关内存的数据结构和相关操作都是直接针对实际存在的资源--物理内存空间的管理,没有从一般应用程序对内存的``需求''考虑,即需要有相关的数据结构和操作来体现一般应用程序对虚拟内存的``需求''。一般应用程序的对虚拟内存的``需求''与物理内存空间的``供给''没有直接的对应关系,ucore是通过缺页异常处理来间接完成这二者之间的衔接。 4 | 5 | 在ucore中描述应用程序对虚拟内存``需求''的数据结构是vma\_struct,以及针对vma\_struct的函数操作。这里把一个vma\_struct结构的变量简称为vma变量。vma\_struct的定义如下: 6 | 7 | \begin{lstlisting} 8 | struct vma_struct { 9 | struct mm_struct *vm_mm; 10 | uintptr_t vm_start; 11 | uintptr_t vm_end; 12 | uint32_t vm_flags; 13 | list_entry_t list_link; 14 | }; 15 | \end{lstlisting} 16 | 17 | vm\_start和vm\_end描述了一个连续地址的虚拟内存空间的起始位置和结束位置,这两个值都应该是 18 | PGSIZE 对齐的,而且描述的是一个合理的地址空间范围(即严格确保 vm\_start 19 | \textless{} vm\_end 20 | 的关系);list\_link是一个双向链表,按照从小到大的顺序把一系列用vma\_struct表示的虚拟内存空间链接起来,并且还要求这些链起来的 21 | vma\_struct 22 | 应该是不相交的,即vma之间的地址空间无交集;vm\_flags表示了这个虚拟内存空间的属性,目前的属性包括: 23 | 24 | \begin{lstlisting} 25 | #define VM_READ 0x00000001 //只读 26 | #define VM_WRITE 0x00000002 //可读写 27 | #define VM_EXEC 0x00000004 //可执行 28 | \end{lstlisting} 29 | 30 | 以后还会引入如 VM\_STACK 的其它属性来支持动态扩展用户栈空间。 31 | 32 | vm\_mm是一个指针,指向一个比vma\_struct更高的抽象层次的数据结构mm\_struct,这里把一个mm\_struct结构的变量简称为mm变量。这个数据结构表示了包含所有虚拟内存空间的共同属性,具体定义如下 33 | 34 | \begin{lstlisting} 35 | struct mm_struct { 36 | list_entry_t mmap_list; 37 | struct vma_struct *mmap_cache; 38 | pde_t *pgdir; 39 | int map_count; 40 | }; 41 | \end{lstlisting} 42 | 43 | mmap\_list是双向链表头,链接了所有属于同一页目录表的虚拟内存空间,mmap\_cache是指向当前正在使用的虚拟内存空间,由于操作系统执行的``局部性''原理,当前正在用到的虚拟内存空间在接下来的操作中可能还会用到,这时就不需要查链表,而是直接使用此指针就可找到下一次要用到的虚拟内存空间。由于 44 | mmap\_cache 的引入,使得 mm\_struct 数据结构的查询加速 30\% 以上。pgdir 45 | 所指向的就是 mm\_struct 46 | 数据结构所维护的页表。通过访问pgdir可以查找某虚拟地址对应的页表项是否存在以及页表项的属性等。map\_count记录 47 | mmap\_list 里面链接的 vma\_struct 的个数。 48 | 49 | 涉及mm\_struct的操作函数比较简单,只有mm\_create和mm\_destroy两个函数,从字面意思就可以看出是是完成mm\_struct结构的变量创建和删除。在mm\_create中用kmalloc分配了一块空间,所以在mm\_destroy中也要对应进行释放。在ucore运行过程中,会产生描述虚拟内存空间的vma\_struct结构,所以在mm\_destroy中也要进对这些mmap\_list中的vma进行释放。涉及vma\_struct的操作函数也比较简单,主要包括三个: 50 | 51 | \begin{itemize} 52 | \item 53 | vma\_create--创建vma 54 | \item 55 | insert\_vma\_struct--插入一个vma 56 | \item 57 | find\_vma--查询vma。 58 | \end{itemize} 59 | 60 | vma\_create函数根据输入参数vm\_start、vm\_end、vm\_flags来创建并初始化描述一个虚拟内存空间的vma\_struct结构变量。insert\_vma\_struct函数完成把一个vma变量按照其空间位置{[}vma-\textgreater{}vm\_start,vma-\textgreater{}vm\_end{]}从小到大的顺序插入到所属的mm变量中的mmap\_list双向链表中。find\_vma根据输入参数addr和mm变量,查找在mm变量中的mmap\_list双向链表中某个vma包含此addr,即vma-\textgreater{}vm\_start\textless{}= 61 | addr end。这三个函数与后续讲到的缺页异常处理有紧密联系。 62 | -------------------------------------------------------------------------------- /latex/wait_awaken_based_time_event.tex: -------------------------------------------------------------------------------- 1 | \section{基于时间事件的等待与唤醒}\label{ux57faux4e8eux65f6ux95f4ux4e8bux4ef6ux7684ux7b49ux5f85ux4e0eux5524ux9192} 2 | 3 | Clock(时钟)中断(irq0,可回顾第二章2.4节) 4 | 可给操作系统提供有一定间隔的时间事件, 5 | 操作系统将其作为基本的计时单位,这里把两次时钟中断之间的时间间隔为一个时间片(timer 6 | splice)。基于此时间片,操作系统得以向上提供基于时间点的事件,并实现基于固定时间长度的等待和唤醒机制。在每个时钟中断发生时,操作系统可产生对应时间长度的时间事件,这样操作系统和应用程序可基于这些时间事件来构建基于时间的软件设计。在proj10.4中,实现了定时器timer的支持。 7 | 8 | \subsection{timer数据结构}\label{timerux6570ux636eux7ed3ux6784} 9 | 10 | sched.h定义了有关timer数据结构, 11 | 12 | \begin{lstlisting} 13 | typedef struct { 14 | unsigned int expires; //到期时间 15 | struct proc_struct *proc; //等待时间到期的进程 16 | list_entry_t timer_link; //链接到timer_list的链表项指针 17 | } timer_t; 18 | \end{lstlisting} 19 | 20 | 这几个成员变量描述了一个timer(定时器)涉及的相关因素,首先一个expires表明了这个定时器何时到期,而第二个成员变量描述了定时器到期后需要唤醒的进程,最后一个参数是一个链表项,用于把自身挂到系统的timer链表上,以便于扫描查找特定的timer。 21 | 22 | \subsection{timer相关操作}\label{timerux76f8ux5173ux64cdux4f5c} 23 | 24 | 一个 timer在ucore中的生存周期可以被描述如下: 25 | 26 | \begin{enumerate} 27 | \def\labelenumi{\arabic{enumi}.} 28 | \item 29 | 某进程创建和初始化timer\_t结构的一个timer,并把timer被加入系统timer管理列表timer\_list中,进程设置为基于timer事件的阻塞态(即睡眠了),这样这个timer就诞生了,; 30 | \item 31 | 系统时间通过时钟中断被不断累加,且ucore定期检查是否有某个timer的到期时间已经到了,如果没有到期,则ucore等待下一次检查,此timer会挂在timer\_list上继续存在; 32 | \item 33 | 如果到期了,则对应的进程又处于就绪态了,并从系统timer管理列表timer\_list中移除该 34 | timer,自此timer就死亡退出了。 35 | \end{enumerate} 36 | 37 | 基于上述timer生存周期的流程,与timer相关的函数如下: 38 | 39 | \begin{itemize} 40 | \item 41 | timer 42 | init:对timer的成员变量进行初始化,设定了在expires时间之后唤醒proc进程 43 | \item 44 | add\_timer:向系统timer链表timer\_list添加某个初始化过的timer,这样该timer计时器将在指定时间expires后被扫描到,如果等待则这个定时器timer的进程处在等待状态,并将将进程唤醒,进程将处于就绪态。 45 | \item 46 | del\_timer:向系统timer链表timer\_list删除(或者说取消)某一个计时器。该计时 47 | 器在取消后,对应的进程不会被系统在指定时刻expires唤醒。 48 | \item 49 | run\_timer\_list:被trap函数调用,遍历系统timer链表timer\_list中的timer计时器,找出所有应该到时的timer计时器,并唤醒与此计时器相关的等待进程,再删除此timer计时器。在lab4/proj13以后,还增加了对进程调度器在时间事件产生后的处理函数的调用(在后续有进一步分析)。 50 | \end{itemize} 51 | 52 | 有了这些函数的支持,我们就可以实现进程睡觉并被定时唤醒的功能了。比如ucore在用户函数库中提供了sleep函数,当用户进程调用sleep函数后,会进一步调用sys\_sleep系统调用,在内核中完成sys\_sleep系统调用服务的是do\_sleep内核函数,其实现如下: 53 | 54 | \begin{lstlisting} 55 | int 56 | do_sleep(unsigned int time) { 57 | …… 58 | timer_t __timer, *timer = timer_init(&__timer, current, time); 59 | current->state = PROC_SLEEPING; 60 | current->wait_state = WT_TIMER; 61 | add_timer(timer); 62 | …… 63 | schedule(); 64 | del_timer(timer); 65 | return 0; 66 | } 67 | \end{lstlisting} 68 | 69 | 可以看出,do\_sleep首先初始化了一个定时器timer,设置了timer的proc是当前进程,到期时间expires是参数time;然后把当前进程的状态设置为等待状态,且等待原因是等某个定时器到期;再调用schedule完成进程调度与切换,这时当前进程已经不占用CPU执行了。当定时器到期后,run\_timer\_list会删除timer且唤醒timer对应的当前进程,从而使得当前进程可以继续执行。 70 | -------------------------------------------------------------------------------- /latex/what_is_thread.tex: -------------------------------------------------------------------------------- 1 | \section{【原理】线程的属性与特征分析}\label{ux539fux7406ux7ebfux7a0bux7684ux5c5eux6027ux4e0eux7279ux5f81ux5206ux6790} 2 | 3 | 线程概念的提出是计算机系统的技术发展和操作系统对进程管理优化过程的自然产物。随着计算机处理能力的提高,内存容量的加大,在一个计算机系统中会存在大量的进程,为了提高整个系统的执行效率,需要进程间能够进行简洁高效的数据共享和进程切换,进程创建和进程退出。这样就会产生一个自然的想法:能否改进进程模型,提供一个简单的数据共享机制,能否加快进程管理(特别是进程切换)的速度?再仔细看看进程管理的核心数据结构进程控制块和处理的流程,就可以发现,在某种情况下,进程的地址空间隔离不一定是一个必须的需求。假定进程间是可以相互``信任''的(即进程间是可信的),那么我们就不必需要地址空间隔离,而是让这些相互信任的进程共用(共享)一个地址空间。这样一下子就解决上述两个问题:在一个地址空间内,一个进程对某内存单元的修改(即写内存单元操作)可以让其他进程马上看见(即读内存单元操作),这是共享地址空间带来的天然好处;另外由于进程共享了地址空间,所以在创建进程或回收进程的时候,只要不是相互信任的进程组的第一个或最后一个,就没必要再创建一个地址空间或回收地址空间,节省了创建进程和退出进程的执行开销;而且对于频繁发生的进程切换操作而言,由于不需要切换页表,所以TLB中缓存的虚拟地址---物理地址映射关系(页表项的缓存)不必清除或失效,减少了进程切换的开销。 4 | 5 | 为了区别已有的进程概念,我们把这些共享资源的进程称为线程。从而把进程的概念进行了细化。原有的进程概念缩小为资源管理的最小单位,而线程是指令执行控制流的最小单位。且线程属于进程,是进程的一部分。一个进程至少需要一个线程作为它的指令执行单元,进程管理主要是资源(如内存空间等)分配、使用和回收的管理,而线程管理主要是指令执行过程和切换过程的管理。一个进程可以拥有多个线程,而这些线程共享其所属进程所拥有的资源。这样,采用多线程模型来设计应用程序,可以使得程序的执行效率也更高。 6 | 7 | 从执行线程调度时CPU所处特权级角度看,有内核级线程模型和用户级线程模型两种线程模型。内核级线程模型由操作系统在核心态进行调度,每个线程有对应的进程控制块结构。用户级线程模型有用户态的线程管理库在用户态进行调度,操作系统不能``感知''到这样的线程存在,所以也就没有对应进程控制块来描述它。相对而言,由于用户级线程模型在执行线程调度切换时不需从用户态转入核心态,开销相对较小,而内核级线程模型虽然开销相对较大,但由于操作系统直接负责管理,所以在执行的灵活性上由于用户级线程模型。比如用户级线程模型中某线程执行系统调用而可能被操作系统阻塞,这会引起同属于一个进程的其他线程都被阻塞。但内核级线程模型不会有这种情况后发生。这两种线程模型还可以结合在一起形成一种``混合''线程模型。``混合''线程模型通常都能带来更高的效率,但也带来更大的实现难度和实现代价。ucore出于``简单''的设计思路,参考Linux实现了内核级线程模型。 8 | 9 | 从线程执行时CPU所处特权级角度看,有内核线程和用户线程之分,内核线程共享操作系统内核运行过程中的所有资源,最主要的就是内核虚拟地址空间,但没有内核线程有各自独立的核心栈,且没有用户态的地址空间。用户线程共享属于同一用户进程的所有资源,最主要的就是用户虚拟地址空间,所以同享同一用户进程的进程控制块所描述的一个页表和一个内存管理数据结构。接下来我们看看ucore中如何具体实现用户线程这个概念。 10 | -------------------------------------------------------------------------------- /makeebooks: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # -*- coding: utf-8 -*- 3 | # This script convers markdown book to one of the serveral e-book 4 | # formats supported with calibre (http://calibre-ebook.com) 5 | # 6 | # Samples: 7 | # 8 | # Build e-book for amazon kindle for english and russian languages 9 | # $ make-ebook en ru 10 | # or 11 | # $ FORMAT=mobi make-ebook en ru 12 | # 13 | # Build e-book in 'epub' format for russian only 14 | # $ FORMAT=epub make-ebook ru 15 | 16 | require 'rubygems' 17 | require 'rdiscount' 18 | require 'ruby-debug' 19 | 20 | if ARGV.length == 0 21 | puts "you need to specify at least one language. For example: makeebooks en" 22 | exit 23 | end 24 | 25 | format = ENV['FORMAT'] || 'mobi' 26 | puts "using .#{format} (you can change it via FORMAT environment variable. try 'mobi' or 'epub')" 27 | 28 | ARGV.each do |lang| 29 | puts "convert content for '#{lang}' language" 30 | 31 | if lang == 'zh' 32 | figure_title = '图' 33 | else 34 | figure_title = 'Figure' 35 | end 36 | 37 | book_content = %(Software Development - Boot Camp) 38 | dir = File.expand_path(File.join(File.dirname(__FILE__), lang)) 39 | Dir[File.join(dir, '**', '*.markdown')].sort.each do |input| 40 | puts "processing #{input}" 41 | content = File.read(input) 42 | content.gsub!(/Insert\s+(.*)(\.png)\s*\n?\s*#{figure_title}\s+(.*)/, '![\3](figures/\1-tn\2 "\3")') 43 | book_content << RDiscount.new(content).to_html 44 | end 45 | book_content << "" 46 | 47 | File.open("sdcamp.#{lang}.html", 'w') do |output| 48 | output.write(book_content) 49 | end 50 | 51 | system('ebook-convert', "sdcamp.#{lang}.html", "sdcamp.#{lang}.#{format}", 52 | '--cover', 'ebooks/cover.png', 53 | '--authors', 'Yu Chen', 54 | '--comments', "licensed under the Creative Commons Attribution-Non Commercial-Share Alike 3.0 license", 55 | '--level1-toc', '//h:h1', 56 | '--level2-toc', '//h:h2', 57 | '--level3-toc', '//h:h3', 58 | '--language', lang) 59 | end 60 | -------------------------------------------------------------------------------- /makepdfs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | baseDir=`dirname $0` 4 | outputDir=${baseDir}/pdf 5 | 6 | exclude=('figures' 'figures-dia' 'figures-source' 'latex' 'makepdfs' 'pdf' 'README') 7 | dirContent=`ls $baseDir` 8 | argString="" 9 | 10 | echo $i; 11 | 12 | for dir in $dirContent; do 13 | if [ -n $dir ]; then 14 | 15 | isLang=1 16 | for i in ${exclude[@]}; do 17 | if [ $i == $dir ]; then 18 | isLang=0 19 | fi 20 | done 21 | 22 | if [ $isLang -eq 1 ]; then 23 | if [ "$1" = "" ]; then 24 | argString="${argString} ${dir}" 25 | else 26 | for i in ${@}; do 27 | if [ $i = $dir ]; then 28 | argString="${argString} ${dir}" 29 | fi 30 | done 31 | fi 32 | fi 33 | fi 34 | done 35 | 36 | echo "Will generate pdf for the following languages:" 37 | echo " "$argString 38 | 39 | mkdir -p $outputDir 40 | 41 | echo 42 | echo "The generation process will start now." 43 | ${baseDir}/latex/makepdf $argString 44 | 45 | # pdftk A=ebooks/cover.pdf B=kaiyuanbook.zh.pdf cat A1 B2-end output kaiyuanbook.zh.book.pdf -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "book", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "gitbook-plugin-autocover": "*" 6 | } 7 | } -------------------------------------------------------------------------------- /sample/codeminimal.tex: -------------------------------------------------------------------------------- 1 | % pdflatex -shell-escape codeminimal.tex 2 | % minted is in texlive-latex-extra package from Ubuntu 12.10 http://packages.ubuntu.com/search?searchon=contents&keywords=minted&mode=filename&suite=quantal&arch=any 3 | % minted is supported in pandoc 1.9.1.2 http://johnmacfarlane.net/pandoc/releases.html 4 | % In Ubuntu 12.04 , pandoc is 1.9.1.1 5 | \documentclass{article} 6 | \usepackage{minted} 7 | \begin{document} 8 | \begin{minted}{c} 9 | int main() { 10 | printf("hello, world"); 11 | return 0; 12 | } 13 | \end{minted} 14 | 15 | \definecolor{bg}{rgb}{0.95,0.95,0.95} 16 | \begin{minted}[bgcolor=bg]{php} 17 | 20 | \end{minted} 21 | 22 | \definecolor{bg}{rgb}{0.95,0.95,0.95} 23 | \begin{minted}[bgcolor=bg]{bash} 24 | $ sudo apt-get install ruby1.9.1 25 | $ sudo apt-get install pandoc 26 | $ sudo apt-get install texlive-xetex 27 | $ sudo apt-get install texlive-latex-recommended # 主要的Latex包 28 | $ sudo apt-get install texlive-latex-extra # titlesec包,先不用知道 29 | \end{minted} 30 | \end{document} -------------------------------------------------------------------------------- /summary.rb: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # 3 | 4 | command = ARGV[0] 5 | exclude = ['figures', 'figures-dia', 'figures-source', 'couchapp', 'latex', 'pdf', 'epub', 'en', 'ebooks'] 6 | 7 | data = [] 8 | original_lines=`grep -r -h '^[^[:space:]#]' en/[0]* | grep -v '^Insert'| wc -l`.to_i 9 | Dir.glob("*").each do |dir| 10 | if !File.file?(dir) && !exclude.include?(dir) 11 | lines = `git diff-tree -r -p --diff-filter=M master:en master:#{dir} | grep '^-[^[:space:]#-]' | grep -v '^-Insert' | wc -l`.strip.to_i 12 | last_commit = `git log -1 --no-merges --format="%ar" #{dir}`.chomp 13 | authors = "" 14 | if command == 'authors' 15 | authors = `git shortlog --no-merges -s -n #{dir}`.chomp 16 | end 17 | data << [dir, lines, authors, last_commit] 18 | end 19 | end 20 | 21 | d = data.sort { |a, b| b[1] <=> a[1] } 22 | d.each do |dir, lines, authors, last| 23 | puts "#{dir.ljust(10)} - #{(lines*100)/original_lines}% (#{last})" 24 | if command == 'authors' 25 | puts "Authors: #{authors.split("\n").size}" 26 | puts authors 27 | puts 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $for(author-meta)$ 8 | 9 | $endfor$ 10 | $if(date-meta)$ 11 | 12 | $endif$ 13 | $if(title-prefix)$$title-prefix$ - $endif$$if(pagetitle)$$pagetitle$$endif$ 14 | $if(highlighting-css)$ 15 | 18 | $endif$ 19 | 20 | 25 | $for(css)$ 26 | 27 | $endfor$ 28 | $if(math)$ 29 | $math$ 30 | $endif$ 31 | $for(header-includes)$ 32 | $header-includes$ 33 | $endfor$ 34 | 35 | 36 | 37 |
38 |
39 | Fork Me on GitHub 40 |

跟我学企业软件开发

41 |

跟我学企业软件开发

42 |
43 | Download this project as a tar.gz file 44 |
45 |
46 |
47 |
48 |
49 | 50 | $if(toc)$ 51 |

目录

52 |
53 | $toc$ 54 |
55 | $endif$ 56 | $body$ 57 | $for(include-after)$ 58 | $include-after$ 59 | $endfor$ 60 | 61 |
62 |
63 | 64 | 65 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /title.zh.txt: -------------------------------------------------------------------------------- 1 | % 跟我学开源技术书 2 | % Larry Cai -------------------------------------------------------------------------------- /upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ls -al *.pdf 3 | pdfv1=`git config remote.origin.url` 4 | pdfv2=${pdfv1#*github.com/} 5 | pdfname=${pdfv2%/*}_kaiyuanbook.zh.pdf 6 | echo $pdfv1 $pdfv2 $pdfname 7 | env 8 | cp id_rsa_mkbok ~/.ssh/id_rsa 9 | chmod 600 ~/.ssh/id_rsa 10 | cat ~/.ssh/known_hosts 11 | echo "repo.or.cz,195.113.20.142 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAs7JJacVNc1wk/6RZqMHin5RwR/LdIcMGGeG6WG4Sl/wETY9KYUVd126Yb2MV7vBT/8dW0iE6u6+sRVM3Xn5MG9K2PvQ57SbIQ53FvR4qBCqYkSn5sKs2wt9GpXh2MFN5TuXth2d1BABSR2a1u461K8SKbhclPVeFCeligaI4lGc=" >> ~/.ssh/known_hosts 12 | cat ~/.ssh/known_hosts 13 | git clone ssh://mkbok@repo.or.cz/srv/git/mkbok.git mkbok.pdf 14 | cd mkbok.pdf 15 | ls -al 16 | cp ../kaiyuanbook.zh.pdf $pdfname 17 | git add $pdfname 18 | git config user.name "Larry Cai" 19 | git config user.email "larry.caiyu@gmail.com" 20 | git commit -am "add file" 21 | git push origin master -------------------------------------------------------------------------------- /zh/.gitignore: -------------------------------------------------------------------------------- 1 | *.tex 2 | -------------------------------------------------------------------------------- /zh/chapter-1/access_harddisk.md: -------------------------------------------------------------------------------- 1 | # 【背景】访问硬盘数据控制 2 | bootloader让80386处理器进入保护模式后,下一步的工作就是从硬盘上加载并运行OS。考虑到实现的简单性,bootloader的访问硬盘都是LBA模式的PIO(Program IO)方式,即所有的I/O操作是通过CPU访问硬盘的I/O地址寄存器完成。 3 | 4 | 一般主板有2个IDE通道(是硬盘的I/O控制器),每个通道可以接2个IDE硬盘。第一个IDE通道通过访问I/O地址0x1f0-0x1f7来实现,第二个IDE通道通过访问0x170-0x17f实现。每个通道的主从盘的选择通过第6个I/O偏移地址寄存器来设置。具体参数见下表。 5 | 6 | I/O地址 功能 7 | 0x1f0 读数据,当0x1f7不为忙状态时,可以读。 8 | 0x1f2 要读写的扇区数,每次读写前,需要指出要读写几个扇区。 9 | 0x1f3 如果是LBA模式,就是LBA参数的0-7位 10 | 0x1f4 如果是LBA模式,就是LBA参数的8-15位 11 | 0x1f5 如果是LBA模式,就是LBA参数的16-23位 12 | 0x1f6 第0~3位:如果是LBA模式就是24-27位 第4位:为0主盘;为1从盘 13 | 第6位:为1=LBA模式;0 = CHS模式 第7位和第5位必须为1 14 | 0x1f7 状态和命令寄存器。操作时先给命令,再读取内容;如果不是忙状态就从0x1f0端口读数据 15 | 16 | 硬盘数据是储存到硬盘扇区中,一个扇区大小为512字节。读一个扇区的流程大致为通过outb指令访问I/O地址:0x1f2~-0x1f7来发出读扇区命令,通过in指令了解硬盘是否空闲且就绪,如果空闲且就绪,则通过inb指令读取硬盘扇区数据都内存中。可进一步参看bootmain.c中的readsect函数实现来了解通过PIO方式访问硬盘扇区的过程。 17 | -------------------------------------------------------------------------------- /zh/chapter-1/bootloader_up_os.md: -------------------------------------------------------------------------------- 1 | # 启动操作系统 2 | 3 | ## 用一句话描述本章 4 | 5 | 站在操作系统的最底层,了解操作系统的启动,与物理硬件:CPU,内存和多种外设实现“零距离”接触,看到它们并管理它们! 6 | 7 | ## 本章收获的知识 8 | 9 | * 与操作系统原理相关 10 | * I/O设备管理:涉及程序循环检测方式和中断启动方式、I/O地址空间 11 | * 内存管理:基于分段机制的内存管理 12 | * 异常处理:涉及中断、故障和陷阱 13 | * 特权级:内核态和用户态 14 | * 计算机系统和编程 15 | * 硬件 16 | * PC从加电到加载操作系统内核的整个过程 17 | * OS内核在内存中的布局 18 | * 并口访问、串口访问、CGA字符显示、硬盘数据访问、时钟访问 19 | * 软件 20 | * ELF执行文件格式 21 | * 栈的实现并实现函数调用栈跟踪函数 22 | * 调试操作系统 23 | 24 | ## 本章涉及的实验 25 | 26 | 读者通过阅读本章的内容并动手实践相关的6个实验项目: 27 | 28 | * proj1:能够切换到保护模式并显示字符的bootloader 29 | * proj2/3:可读ELF格式文件的bootloader和显示字符的ucore 30 | * proj3.1:内置监控自身运行状态的ucore 31 | * proj4:可管理中断和处理基于中断的键盘/时钟的ucore 32 | * proj4.1.1:支持通过中断方式的内核态/用户态切换的ucore 33 | 34 | ## 本章概述 35 | 36 | 其实这一章的内容与操作系统原理相关的部分较少,与计算机体系结构(特别是x86)的细节相关的部分较多。但这些内容对写一个操作系统关系较大,要知道操作系统是直接与硬件打交道的软件,所以它需要“知道”需要硬件细节,才能更好地控制硬件。另一方面,部分内容涉及到操作系统的重要抽象--中断类异常,能够充分理解中断类异常为以后进一步了解进程切换、上下文切换等概念会很有帮助。 37 | 38 | 本章的实验内容涉及的是写一个bootloader能够启动一个操作系统--ucore。在完成bootloader的过程中,逐渐增加bootloader和ucore的能力,涉及x86处理器的保护模式切换、解析ELF执行文件格式等,这对于理解操作系统的加载过程以及在操作系统在内存中的位置、内存管理、用户态与内核态的区别等有帮助。而相关project中bootloader和操作系统本身的字符显示的I/O处理、读硬盘数据的I/O处理、键盘/时钟的中断处理等内容,则是操作系统原理中一般在靠后位置提到的设备管理的实际体现。纵观操作系统的发展史,从早期到现在的操作系统主要功能之一就是完成繁琐的I/O处理,给上层应用提供比较简洁的I/O服务,屏蔽硬件处理的复杂性。这也是操作系统的虚拟机功能的体现。另外,本章还介绍了对硬件模拟器的使用,对操作系统的panic处理和远程debug功能的支持,这样有助于读者能够方便地分析操作系统中的错误和调试操作系统。由于本章涉及的硬件知识较多,无疑增大了读者的阅读难度,需要读者在结合阅读本章并实际动手实验来进行深入理解。 39 | 40 | -------------------------------------------------------------------------------- /zh/chapter-1/figures/3.13.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-1/figures/3.13.1.png -------------------------------------------------------------------------------- /zh/chapter-1/figures/3.13.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-1/figures/3.13.2.png -------------------------------------------------------------------------------- /zh/chapter-1/figures/3.13.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-1/figures/3.13.3.png -------------------------------------------------------------------------------- /zh/chapter-1/figures/3.13.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-1/figures/3.13.4.png -------------------------------------------------------------------------------- /zh/chapter-1/figures/3.13.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-1/figures/3.13.5.png -------------------------------------------------------------------------------- /zh/chapter-1/figures/3.15.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-1/figures/3.15.1.png -------------------------------------------------------------------------------- /zh/chapter-1/figures/3.15.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-1/figures/3.15.2.png -------------------------------------------------------------------------------- /zh/chapter-1/figures/3.15.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-1/figures/3.15.3.png -------------------------------------------------------------------------------- /zh/chapter-1/figures/3.18.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-1/figures/3.18.1.png -------------------------------------------------------------------------------- /zh/chapter-1/figures/3.2.4.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-1/figures/3.2.4.1.png -------------------------------------------------------------------------------- /zh/chapter-1/figures/3.2.4.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-1/figures/3.2.4.2.png -------------------------------------------------------------------------------- /zh/chapter-1/figures/3.2.7.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-1/figures/3.2.7.1.png -------------------------------------------------------------------------------- /zh/chapter-1/figures/3.2.7.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-1/figures/3.2.7.2.png -------------------------------------------------------------------------------- /zh/chapter-1/figures/os-position.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-1/figures/os-position.png -------------------------------------------------------------------------------- /zh/chapter-1/figures/qemu_cha1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-1/figures/qemu_cha1.jpg -------------------------------------------------------------------------------- /zh/chapter-1/poweron.md: -------------------------------------------------------------------------------- 1 | # 【背景】Intel 80386加电后启动过程 2 | 3 | **【要点(非OSP):80836物理内存地址空间】** 4 | 5 | **【要点(非OSP):80836加电后的第一条指令位】** 6 | 7 | 大家一般都知道bootloader负责启动操作系统,但bootloader自身是被谁加载并启动的呢?为了追根溯源,我们需要了解当计算机加电启动后,到底发生了什么事情。 8 | 9 | 对于绝大多数计算机系统而言,操作系统和应用软件是存放在磁盘(硬盘/软盘)、光盘、EPROM、ROM、Flash等可在掉电后继续保存数据的存储介质上。当计算机加电后,一般不直接执行操作系统,而是一开始会到一个特定的地址开始执行指令,这个特定的地址存放了系统初始化软件,通过执行系统初始化软件(可固化在ROM或Flash中,也称firmware,固件)完成基本I/O初始化和引导加载操作系统的功能。简单地说,系统初始化软件就是在操作系统内核运行之前运行的一段小软件。通过这段小软件的基本I/O初始化部分,我们可以初始化硬件设备、建立系统的内存空间映射图,从而将系统的软硬件环境带到一个合适的状态,以便为最终调用操作系统内核准备好正确的环境。最终系统初始化软件的引导加载部分把操作系统内核映像加载到RAM中,并将系统控制权传递给它。 10 | 11 | 对于基于Intel 80386的计算机而言,其中的系统初始化软件由BIOS (Basic Input Output System,即基本输入/输出系统,其本质是一个固化在主板Flash/CMOS上的软件)和位于软盘/硬盘引导扇区中的OS Boot Loader(在ucore中的bootasm.S和bootmain.c)一起组成。BIOS实际上是被固化在计算机ROM(只读存储器)芯片上的一个特殊的软件,为上层软件提供最底层的、最直接的硬件控制与支持。 12 | 13 | 以基于Intel 80386的计算机为例,计算机加电后,整个物理地址空间如下图所示: 14 | 15 | ![3.13.1.png](figures/3.13.1.png) 16 | 17 | 图2-1 基于Intel 80386的计算机物理地址空间 18 | 19 | 处理器处于实模式状态(在86386中,段机制一直存在,可进一步参考2.1.5 【背景】理解保护模式和分段机制),从物理地址0xFFFFFFF0开始执行。初始化状态的CS和EIP确定了处理器的初始执行地址,此时CS中可见部分-选择子(selector)的值为0xF000,而其不可见部分-基地址(base)的值为0xFFFF0000;EIP的值是0xFFF0,这样实际的线性地址(由于没有启动也机制,所以线性地址就是物理地址)为CS.base+EIP=0xFFFFFFF0。在0xFFFFFFF0这里只是存放了一条跳转指令,通过跳转指令跳到BIOS例行程序起始点。更详细的解释可以参考文献[1]的第九章的9.1节“INITIALIZATION OVERVIEW”。另外,我们可以通过硬件模拟器qemu来进一步认识上述结果。 20 | 21 | ### 实验2-1:通过qemu了解Intel 80386启动后的CS和EIP值,并分析第一条指令的内容 22 | 23 | 1. 启动qemu并让其停到执行第一条指令前,这需要增加一个参数”-S” 24 | qemu –S 25 | 2. 这是qemu会弹出一个没有任何显示内容的图形窗口,显示如下: 26 | 27 | ![3.13.2.png](figures/3.13.2.png) 28 | 29 | 3. 然后通过按”Ctrl+Alt+2”进入qemu的monitor界面,为了了解80386此时的寄存器内容,在monitor界面下输入命令 “info registers” 30 | 31 | ![3.13.3.png](figures/3.13.3.png) 32 | 33 | 4. 可获得intel 80386启动后执行第一条指令前的寄存器内容,如下图所示 34 | 35 | ![3.13.4.png](figures/3.13.4.png) 36 | 37 | 从上图中,我们可以看到EIP=0xfff0,CS的selector=0xf000,CS的base=0xfff0000。 38 | 39 | BIOS做完计算机硬件自检和初始化后,会选择一个启动设备(例如软盘、硬盘、光盘等),并且读取该设备的第一扇区(即主引导扇区或启动扇区)到内存一个特定的地址0x7c00处,然后CPU控制权会转移到那个地址继续执行。至此BIOS的初始化工作做完了,进一步的工作交给了ucore的bootloader;ucore的bootloader会完成处理器从实模式到保护模式的转换,并从硬盘上读取并加载ucore。其大致流程如下图所示: 40 | 41 | ![3.13.5.png](figures/3.13.5.png) 42 | 43 | 图2-2 Intel80386启动过程 44 | -------------------------------------------------------------------------------- /zh/chapter-1/reference.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-1/reference.md -------------------------------------------------------------------------------- /zh/chapter-1/setup_stack.md: -------------------------------------------------------------------------------- 1 | # 【实现】设置栈 2 | 3 | 只有设置好的合适大小和地址的栈内存空间(简称栈空间),才能有效地进行函数调用。这里为了减少汇编代码量,我们就通过C代码来完成显示。由于需要调用C语言的函数,所以需要自己建立好栈空间。设置栈的代码如下: 4 | 5 | movl $start, %esp 6 | 7 | 由于start位置(0x7c00)前的地址空间没有用到,所以可以用来作为bootloader的栈,需要注意栈是向下长的,所以不会破坏start位置后面的代码。在后面的小节还会对栈进行更加深入的讲解。我们可以通过用gdb调试bootloader来进一步观察栈的变化: 8 | 9 | **【实验】用gdb调试bootloader观察栈信息 ** 10 | 11 | 1. 开两个窗口;在一个窗口中,在proj1目录下执行命令make; 12 | 2. 在proj1目录下执行 “qemu -hda bin/ucore.img -S -s”,这时会启动一个qemu窗口界面,处于暂停状态,等待gdb链接; 13 | 3. 在另外一个窗口中,在proj1目录下执行命令 gdb obj/bootblock.o; 14 | 4. 在gdb的提示符下执行如下命令,会有一定的输出: 15 | ``` 16 | (gdb) target remote :1234 #与qemu建立远程链接 17 | (gdb) break bootasm.S:68 #在bootasm.S的第68行“movl $start, %esp”设置一个断点 18 | (gdb) continue #让qemu继续执行 19 | ``` 20 | 这时qemu会继续执行,但执行到bootasm.S的第68行时会暂停,等待gdb的控制。这时可以在gdb中继续输入如下命令来分析栈的变化: 21 | ``` 22 | (gdb) info registers esp 23 | esp 0xffd6 0xffd6 #没有执行第68行代码前的esp值 24 | (gdb) si #执行第68行代码 25 | 69 call bootmain 26 | (gdb) info registers esp 27 | esp 0x7c00 0x7c00 #当前的esp值,即栈顶 28 | (gdb) si 29 | bootmain () at boot/bootmain.c:87 #执行call汇编指令 30 | 87 bootmain(void) { 31 | (gdb) info registers esp 32 | esp 0x7bfc 0x7bfc #当前的esp值0x7bfc, 0x7bfc处存放了bootmain函数的返回地址0x7c4a,这可以通过下面两个命令了解 33 | (gdb) x /4x 0x7bfc 34 | 0x7bfc: 0x00007c4a 0xc031fcfa 0xc08ed88e 0x64e4d08e 35 | (gdb) x /4i 0x7c40 36 | 0x7c40 : mov $0x7c00,%esp 37 | 0x7c45 : call 0x7c6c 38 | 0x7c4a : jmp 0x7c4a 39 | 0x7c4c : add %al,(%eax) 40 | ``` 41 | 42 | ##【提示】 43 | 44 | 在proj1中执行 45 | ``` 46 | make debug 47 | ``` 48 | 则自动完成上述大部分前期工作,即qemu和gdb的加载,且gdb会自动建立于qemu的联接并设置好断点。具体实现可参看proj1的Makefile中于debug相关的内容和tools/gdbinit中的内容。 49 | -------------------------------------------------------------------------------- /zh/chapter-1/show_string.md: -------------------------------------------------------------------------------- 1 | # 【实现】显示字符串 2 | 3 | bootloader只在CPU和内存中打转无法让读者很容易知道bootloader的工作是否正常,为此在成功完成了保护模式的转换后,就需要通过显示字符串来展示一下自己了。bootloader设置好栈后,就可以调用bootmain函数显示字符串了。在proj1中使用了显示器和并口两种外设来显示字符串,主要的代码集中在bootmain.c中。 4 | 5 | 这里采用的是很简单的基于Programmed I/O (PIO)方式,PIO方式是一种通过CPU执行I/O端口指令来进行数据读写的数据交换模式,被广泛应用于硬盘、光驱等设备的基础传输模式中。这种I/O访问方式使用CPU I/O端口指令来传送所有的命令、状态和数据,需要CPU全程参与,效率较低,但编程很简单。后面讲到的中断方式将更加高效。 6 | 在bootmain.c中的lpt_putc函数完成了并口输出字符的工作。输出一个字符的流程(可参看bootmain.c中的lpc_putc函数实现)大致如下: 7 | 8 | 1. 读I/O端口地址0x379,等待并口准备好; 9 | 2. 向I/O端口地址0x378发出要输出的字符; 10 | 3. 向I/O端口地址0x37A发出控制命令,让并口处理要输出的字符。 11 | 12 | 在bootmain.c中的serial_putc函数完成了串口输出字符的工作。输出一个字符的流程(可参看bootmain.c中的serial_putc函数实现)大致如下: 13 | 14 | 1. 读I/O端口地址(0x3f8+5)获得LSR寄存器的值,等待串口输出准备好; 15 | 2. 向I/O端口地址0x3f8发出要输出的字符; 16 | 17 | 在bootmain.c中的cga_putc函数完成了CGA字符方式在某位置输出字符的工作。输出一个字符的流程(可参看bootmain.c中的cga_putc函数实现)大致如下: 18 | 19 | 1. 写I/O端口地址0x3d4,读I/O端口地址0x3d5,获得当前光标位置; 20 | 2. 在光标的下一位置的显存地址空间上写字符,格式是黑色背景/白色字符; 21 | 3. 设置当前光标位置为下一位置。 22 | 23 | proj1启动后的PC机内存布局如下图所示: 24 | 25 | ![3.18.1](figures/3.18.1.png) 26 | 27 | 自此,我们了解了一个小巧的bootloader的实现过程,但这还仅仅是百尺竿头的第一步,它还只能显示字符串,不能加载操作系统。我们还需要扩展bootloader的功能,让它能够加载操作系统。 28 | -------------------------------------------------------------------------------- /zh/chapter-1/show_string_in_ucore.md: -------------------------------------------------------------------------------- 1 | # 【实现】可输出字符串的ucore 2 | 3 | proj3包含了一个只能输出字符串的简单ucore操作系统,虽然简单,但它也体现了操作系统的一些结构和特征,比如它具有: 4 | 5 | * 完成给ucore的BSS段清零并显示一个字符串的内核初始化子系统(init.c) 6 | * 提供串口/并口/CGA显示的驱动程序子系统(console.c) 7 | * 提供公共服务的操作系统函数库子系统(printf.c printfmt.c string.c) 8 | 9 | 这体现了操作系统的一个基本特征:资源管理器。从操作系统原理我们可以知道一台计算机就是一组资源,这些资源用于对数据的移动、存储和处理并进行控制。在proj3中的ucore操作系统目前只提供了对串口/并口/CGA这三种I/O设备的硬件资源的访问,每个I/O设备的操作都有自己特有的指令集或控制信号(对照一下serial_putc/lpt_putc/cga_putc函数的实现),操作系统隐藏这些细节,并提供了统一的接口(看看cprintf函数的实现),因此程序员可以使用简单的printf函数来写这些设备,达到显示数据的效果。目前操作系统的逻辑结构图架构如下图所示: 10 | 11 | ![3.2.7.1](figures/3.2.7.1.png) 12 | 13 | 在PC中的地址空间布局图如下所示: 14 | 15 | ![3.2.7.2](figures/3.2.7.2.png) 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /zh/chapter-1/summary.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /zh/chapter-1/ucore_code.md: -------------------------------------------------------------------------------- 1 | # 【背景】操作系统执行代码的组成 2 | 3 | ucore通过gcc编译和ld链接,形成了ELF格式执行文件kernel(位于bin目录下),这样kernel的内部组成与一般的应用程序差别不大。一般而言,一个执行程序的内容是至少由 bss段、data段、text段三大部分组成。 4 | * BSS段:BSS(Block Started by Symbol)段通常是指用来存放执行程序中未初始化的全局变量的一块存储区域。BSS段属于静态内存分配的存储空间。 5 | * 数据段:数据段(Data Segment)通常是指用来存放执行程序中已初始化的全局变量的一块存储区域。数据段属于静态内存分配的存储空间。 6 | * 代码段:代码段(Code Segment/Text Segment)通常是指用来存放程序执行代码的一块存储区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些CPU架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。 7 | 8 | ucore和一般应用程序一样,首先是保存在像硬盘这样的非易失性存储介质上,当需要运行时,被加载到内存中。这时,需要把代码段、数据段的内容拷贝到内存中。对于位于BSS段中的未初始化的全局变量,执行程序一般认为其值为零。所以需要把BSS段对应的内存空间清零,确保执行代码的正确运行。可查看init文件中的kern_init函数的第一个执行语句“memset(edata, 0, end - edata);”。 9 | 10 | 随着ucore的执行,可能需要进行函数调用,这就需要用到栈(stack);如果需要动态申请内存,这就需要用到堆(heap)。堆和栈是在操作系统执行过程中动态产生和变化的,并不存在于表示内核的执行文件中。栈又称堆栈, 是用户存放程序临时创建的局部变量,即函数中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用函数的栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进后出特点,所以栈特别方便用来保存/恢复调用现场。可以把栈看成一个寄存、交换临时数据的内存区。堆是用于存放运行中被动态分配的内存空间,它的大小并不固定,可动态扩张或缩减,这需要操作系统自己进行有效的管理。 11 | -------------------------------------------------------------------------------- /zh/chapter-2/ISR_in_ucore.md: -------------------------------------------------------------------------------- 1 | # 【实现】中断处理过程 2 | 3 | 当中断产生后,首先硬件要完成一系列的工作(如小节“中断处理中硬件负责完成的工作”所描述的“硬件中断处理过程1(起始)”内容),由于中断发生在内核态执行过程中,所以特权级没有变化,所以CPU在跳转到中断处理例程之前,还会在内核栈中依次压入错误码(可选)、EIP、CS和EFLAGS,下图显示了在相同特权级下中断产生后的栈变化示意图: 4 | 5 | ![3.4.7.1](figures/3.4.7.1.png) 6 | 7 | 然后CPU就跳转到IDT中记录的中断号i所对应的中断服务例程入口地址处继续执行。 vector.S 文件中定义了每个中断的中断处理例程的入口地址 (保存在 vectors 数组中)。其中,中断可以分成两类:一类是压入错误编码的 (error code),另一类不压入错误编码。对于第二类, vector.S 自动压入一个 0。此外,还会压入相应中断的中断号。在内核栈中压入一个或两个必要的参数之后,都会跳转到统一的入口 \__alltraps 处(位于trapentry.S中)继续执行。 8 | 9 | CPU从_\_alltraps处开始,在栈中按照trapframe结构压入各个寄存器,此时内核栈的结构如下所示: 10 | 11 | uint32_t reg_edi; 12 | uint32_t reg_esi; 13 | uint32_t reg_ebp; 14 | uint32_t reg_oesp; /* Useless */ 15 | uint32_t reg_ebx; 16 | uint32_t reg_edx; 17 | uint32_t reg_ecx; 18 | uint32_t reg_eax; 19 | uint16_t tf_es; 20 | uint16_t tf_padding1; 21 | uint16_t tf_ds; 22 | uint16_t tf_padding2; 23 | uint32_t tf_trapno; 24 | /* below here defined by x86 hardware */ 25 | uint32_t tf_err; 26 | uintptr_t tf_eip; 27 | uint16_t tf_cs; 28 | uint16_t tf_padding3; 29 | uint32_t tf_eflags; 30 | 31 | 此时,为了将来能够恢复被打断的内核执行过程所需的寄存器内容都保存好了。为了正确进行中断处理,把DS和ES寄存器设置为GD_KDATA,这是为了预防从用户态产生的中断(当然,到目前为止,ucore都在内核态执行,还不会发生这种情况)。把刚才保存的trapframe结构的起始地址(即当前SP值)压栈,然后调用 trap函数(定义在trap.c中),就开始了对具体中断的处理。trap进一步调用trap_dispatch函数,完成对具体中断的处理。在相应的处理过程结束以后,trap将会返回,在\__trapret:中,完成对返回前的寄存器和栈的回复准备工作,最后通过iret指令返回到中断打断的地方继续执行。整个中断处理流程大致如下: 32 | 33 | ![3.4.7.2](figures/3.4.7.2.png) 34 | 35 | ![3.4.7.3](figures/3.4.7.3.png) 36 | 37 | -------------------------------------------------------------------------------- /zh/chapter-2/figures/3.3.3.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-2/figures/3.3.3.1.png -------------------------------------------------------------------------------- /zh/chapter-2/figures/3.4.2.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-2/figures/3.4.2.1.png -------------------------------------------------------------------------------- /zh/chapter-2/figures/3.4.3.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-2/figures/3.4.3.1.png -------------------------------------------------------------------------------- /zh/chapter-2/figures/3.4.3.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-2/figures/3.4.3.2.png -------------------------------------------------------------------------------- /zh/chapter-2/figures/3.4.3.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-2/figures/3.4.3.3.png -------------------------------------------------------------------------------- /zh/chapter-2/figures/3.4.3.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-2/figures/3.4.3.4.png -------------------------------------------------------------------------------- /zh/chapter-2/figures/3.4.3.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-2/figures/3.4.3.5.png -------------------------------------------------------------------------------- /zh/chapter-2/figures/3.4.7.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-2/figures/3.4.7.1.png -------------------------------------------------------------------------------- /zh/chapter-2/figures/3.4.7.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-2/figures/3.4.7.2.png -------------------------------------------------------------------------------- /zh/chapter-2/figures/3.4.7.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-2/figures/3.4.7.3.png -------------------------------------------------------------------------------- /zh/chapter-2/figures/3.5.2.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-2/figures/3.5.2.1.png -------------------------------------------------------------------------------- /zh/chapter-2/figures/3.5.5.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-2/figures/3.5.5.1.png -------------------------------------------------------------------------------- /zh/chapter-2/init_IDT.md: -------------------------------------------------------------------------------- 1 | #【实现】初始化中断门描述符表 2 | 3 | ucore操作系统如果要正确处理各种不同的中断事件,就需要安排应该由哪个中断服务例程负责处理特定的中断事件。系统将所有的中断事件统一进行了编号(0~255),这个编号称为中断号或中断向量。 4 | 5 | 为了完成中断号和中断服务例程起始地址的对应关系,首先需要建立256个中断处理例程的入口地址。为此,通过一个 C程序 tools/vector.c 生成了一个文件vectors.S,在此文件中的 \__vectors地址处开始处连续存储了256个中断处理例程的入口地址数组,且在此文件中的每个中断处理例程的入口地址处,实现了中断处理过程的第一步初步处理。 6 | 7 | 有了中断服务例程的起始地址,就可以建立对应关系了,这部分的实现在trap.c文件中的idt_init函数中实现: 8 | 9 | //全局变量:中断门描述符表 10 | 11 | static struct gatedesc idt[256] = {{0}}; 12 | …… 13 | void idt_init(void) { 14 | 15 | //保存在vectors.S中的256个中断处理例程的入口地址数组 16 | 17 | extern uint32_t __vectors[]; 18 | int i; 19 | 20 | //在中断门描述符表中通过建立中断门描述符,其中存储了中断处理例程的代码段GD_KTEXT和偏移量\__vectors[i],特权级为DPL_KERNEL。这样通过查询idt[i]就可定位到中断服务例程的起始地址。 21 | 22 | for (i = 0; i < sizeof(idt) / sizeof(struct gatedesc); i ++) { 23 | SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL); 24 | } 25 | 26 | //建立好中断门描述符表后,通过指令lidt把中断门描述符表的起始地址装入IDTR寄存器中,从而完成中段描述符表的初始化工作。 27 | 28 | lidt(&idt_pd); 29 | } 30 | 31 | -------------------------------------------------------------------------------- /zh/chapter-2/init_intr_controller.md: -------------------------------------------------------------------------------- 1 | # 【实现】初始化中断控制器 2 | 3 | 80386把中断号0~31分配给陷阱、故障和非屏蔽中断,而把32~47之间的中断号分配给可屏蔽中断。可屏蔽中断的中断号是通过对中断控制器的编程来设置的。下面描述了对8259A中断控制器初始化过程。 4 | 5 | 8259A通过两个I/O地址来进行中断相关的数据传送,对于单个的8259A或者是两级级联中的主8259A而言,这两个I/O地址是0x20和0x21。对于两级级联的从8259A而言,这两个I/O地址是0xA0和0xA1。8259A有两种编程方式,一是初始化方式,二是工作方式。在操作系统启动时,需要对8959A做一些初始化工作,即实现8259A的初始化方式编程。8259A中的四个中断命令字(ICW)寄存器用来完成初始化编程,其含义如下: 6 | 7 | * ICW1:初始化命令字。 8 | * ICW2:中断向量寄存器,初始化时写入高五位作为中断向量的高五位,然后在中断响应时由8259根据中断源(哪个管脚)自动填入形成完整的8位中断向量(或叫中断类型号)。 9 | * ICW3: 8259的级联命令字,用来区分主片和从片。 10 | * ICW4:指定中断嵌套方式、数据缓冲选择、中断结束方式和CPU类型。 11 | 12 | 8259A初始化的过程就是写入相关的命令字,8259A内部储存这些命令字,以控制8259A工作。有关的硬件可看附录补充资料。这里只把ucore对8259A的初始化过程(在picirq.c中的pic_init函数实现)描述一下: 13 | 14 | //此时系统尚未初始化完毕,故屏蔽主从8259A的所有中断 15 | 16 | outb(IO_PIC1 + 1, 0xFF); 17 | outb(IO_PIC2 + 1, 0xFF); 18 | 19 | // 设置主8259A的ICW1,给ICW1写入0x11,0x11表示(1)外部中断请求信号为上升沿触发有效,(2)系统中有多片8295A级联,(3)还表示要向ICW4送数据 20 | 21 | // ICW1设置格式为: 0001g0hi 22 | // g: 0 = edge triggering, 1 = level triggering 23 | // h: 0 = cascaded PICs, 1 = master only 24 | // i: 0 = no ICW4, 1 = ICW4 required 25 | 26 | outb(IO_PIC1, 0x11); 27 | 28 | // 设置主8259A的ICW2: 给ICW2写入0x20,设置中断向量偏移值为0x20,即把主8259A的IRQ0-7映射到向量0x20-0x27 29 | 30 | outb(IO_PIC1 + 1, IRQ_OFFSET); 31 | 32 | // 设置主8259A的ICW3: ICW3是8259A的级联命令字,给ICW3写入0x4,0x4表示此主中断控制器的第2个IR线(从0开始计数)连接从中断控制器。 33 | 34 | outb(IO_PIC1 + 1, 1 << IRQ_SLAVE); 35 | 36 | //设置主8259A的ICW4:给ICW4写入0x3,0x3表示采用自动EOI方式,即在中断响应时,在8259A送出中断矢量后,自动将ISR相应位复位;并且采用一般嵌套方式,即当某个中断正在服务时,本级中断及更低级的中断都被屏蔽,只有更高的中断才能响应。 37 | 38 | // ICW4设置格式为: 000nbmap 39 | // n: 1 = special fully nested mode 40 | // b: 1 = buffered mode 41 | // m: 0 = slave PIC, 1 = master PIC 42 | // (ignored when b is 0, as the master/slave role 43 | // can be hardwired). 44 | // a: 1 = Automatic EOI mode 45 | // p: 0 = MCS-80/85 mode, 1 = intel x86 mode 46 | outb(IO_PIC1 + 1, 0x3); 47 | 48 | //设置从8259A的ICW1:含义同上 49 | 50 | outb(IO_PIC2, 0x11); // ICW1 51 | 52 | //设置从8259A的ICW2:给ICW2写入0x28,设置从8259A的中断向量偏移值为0x28 53 | 54 | outb(IO_PIC2 + 1, IRQ_OFFSET + 8); // ICW2 55 | 56 | //0x2表示此从中断控制器链接主中断控制器的第2个IR线 57 | 58 | outb(IO_PIC2 + 1, IRQ_SLAVE); // ICW3 59 | 60 | //设置主8259A的ICW4:含义同上 61 | 62 | outb(IO_PIC2 + 1, 0x3); // ICW4 63 | 64 | //设置主从8259A的OCW3:即设置特定屏蔽位(值和英文解释不一致),允许中断嵌套;不查询;将读入其中断请求寄存器IRR的内容 65 | 66 | // OCW3设置格式为: 0ef01prs 67 | // ef: 0x = NOP, 10 = clear specific mask, 11 = set specific mask 68 | // p: 0 = no polling, 1 = polling mode 69 | // rs: 0x = NOP, 10 = read IRR, 11 = read ISR 70 | outb(IO_PIC1, 0x68); // clear specific mask 71 | outb(IO_PIC1, 0x0a); // read IRR by default 72 | 73 | outb(IO_PIC2, 0x68); // OCW3 74 | outb(IO_PIC2, 0x0a); // OCW3 75 | 76 | //初始化完毕,使能主从8259A的所有中断 77 | 78 | if (irq_mask != 0xFFFF) { 79 | pic_setmask(irq_mask); 80 | } -------------------------------------------------------------------------------- /zh/chapter-2/init_intr_in_device.md: -------------------------------------------------------------------------------- 1 | # 【实现】外设的相关中断初始化 2 | 3 | 串口的初始化函数serial_init(位于/kern/driver/console.c)中涉及中断初始化工作的很简单: 4 | 5 | ...... 6 | // 使能串口1接收字符后产生中断 7 | outb(COM1 + COM_IER, COM_IER_RDI); 8 | ...... 9 | // 通过中断控制器使能串口1中断 10 | pic_enable(IRQ_COM1); 11 | 12 | 键盘的初始化函数kbd_init(位于kern/driver/console.c中)完成了对键盘的中断初始化工作,具体操作更加简单: 13 | 14 | ...... 15 | // 通过中断控制器使能键盘输入中断 16 | pic_enable(IRQ_KBD); 17 | 18 | 时钟是一种有着特殊作用的外设,其作用并不仅仅是计时。在后续章节中将讲到,正是由于有了规律的时钟中断,才使得无论当前CPU运行在哪里,操作系统都可以在预先确定的时间点上获得CPU控制权。这样当一个应用程序运行了一定时间后,操作系统会通过时钟中断获得CPU控制权,并可把CPU资源让给更需要CPU的其他应用程序。时钟的初始化函数clock_init(位于kern/driver/clock.c中)完成了对时钟控制器8253的初始化: 19 | 20 | ...... 21 | //设置时钟每秒中断100次 22 | outb(IO_TIMER1, TIMER_DIV(100) % 256); 23 | outb(IO_TIMER1, TIMER_DIV(100) / 256); 24 | // 通过中断控制器使能时钟中断 25 | pic_enable(IRQ_TIMER); -------------------------------------------------------------------------------- /zh/chapter-2/kernel_to_user.md: -------------------------------------------------------------------------------- 1 | # 【实现】内核态切换到用户态 2 | 3 | 在kern/init.c中的switch_test函数完成了内核态<-->用户态之间的切换。内核态切换到用户态是通过swtich_to_user函数,执行指令“int T_SWITCH_TOU”。当CPU执行这个指令时,由于是在switch_to_user执行在内核态,所以不存在特权级切换问题,硬件只会在内核栈中压入Error Code(可选)、EIP、CS和EFLAGS(如下图所示),然后跳转到到IDT中记录的中断号T_SWITCH_TOU所对应的中断服务例程入口地址处继续执行。通过2.3.7小节“中断处理过程”可知,会执行到trap_disptach函数(位于trap.c): 4 | 5 | case T_SWITCH_TOU: 6 | if (tf->tf_cs != USER_CS) { 7 | //当前在内核态,需要建立切换到用户态所需的trapframe结构的数据switchk2u 8 | switchk2u = *tf; 9 | switchk2u.tf_cs = USER_CS; 10 | switchk2u.tf_ds = switchk2u.tf_es = switchk2u.tf_ss = USER_DS; 11 | switchk2u.tf_esp = (uint32_t)tf + sizeof(struct trapframe) - 8; 12 | //设置EFLAG的I/O特权位,使得在用户态可使用in/out指令 13 | switchk2u.tf_eflags |= (3 << 12); 14 | //设置临时栈,指向switchk2u,这样iret返回时,CPU会从switchk2u恢复数据, 15 | //而不是从现有栈恢复数据。 16 | *((uint32_t *)tf - 1) = (uint32_t)&switchk2u; 17 | } 18 | 19 | 这样在trap将会返回,在\__trapret:中,根据switchk2u的内容完成对返回前的寄存器和栈的回复准备工作,最后通过iret指令,CPU返回“int T_SWITCH_TOU”的后一条指令处,以用户态模式继续执行。 20 | 21 | ![3.5.5.1](figures/3.5.5.1.png) 22 | 23 | -------------------------------------------------------------------------------- /zh/chapter-2/osprinciple_control_computer.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-2/osprinciple_control_computer.md -------------------------------------------------------------------------------- /zh/chapter-2/privilege_level.md: -------------------------------------------------------------------------------- 1 | # 【背景】分段机制的特权限制 2 | 3 | 在保护模式下,特权级总共有4个,编号从0(最高特权)到3(最低特权)。三类主要的资源,即内存、I/O地址空间以及特权指令需要保护。特权指令如果被用户态的程序所使用,就会受到保护模式的保护机制限制,导致一个故障中断(general-protection exception)。对内存和I/O端口的访问存在类似的特权级限制。为了更好地理解不同特权级,这里先介绍三个概念 4 | 5 | * CPL:当前特权级(Current Privilege Level) 保存在CS段寄存器(选择子)的最低两位,CPL就是当前活动代码段的特权级,并且它定义了当前所执行程序的特权级别) 6 | * DPL:描述符特权(Descriptor Privilege Level) 存储在段描述符中的权限位,用于描述对应段所属的特权等级,也就是段本身真正的特权级。 7 | * RPL:请求特权级RPL(Request Privilege Level) RPL保存在选择子的最低两位。RPL说明的是进程对段访问的请求权限,意思是当前进程想要的请求权限。RPL的值由程序员自己来自由的设置,并不一定RPL>=CPL,但是当RPL\tf_cs != KERNEL_CS) { 7 | //发出中断时,CPU处于用户态,我们希望处理完此中断后,CPU继续在内核态运行, 8 | //所以把tf->tf_cs和tf->tf_ds都设置为内核代码段和内核数据段 9 | tf->tf_cs = KERNEL_CS; 10 | tf->tf_ds = tf->tf_es = KERNEL_DS; 11 | //设置EFLAGS,让用户态不能执行in/out指令 12 | tf->tf_eflags &= ~(3 << 12); 13 | 14 | switchu2k = (struct trapframe *)(tf->tf_esp - (sizeof(struct trapframe) - 8)); 15 | //设置临时栈,指向switchu2k,这样iret返回时,CPU会从switchu2k恢复数据, 16 | //而不是从现有栈恢复数据。 17 | memmove(switchu2k, tf, sizeof(struct trapframe) - 8); 18 | *((uint32_t *)tf - 1) = (uint32_t)switchu2k; 19 | } 20 | break; 21 | 22 | 这样在trap将会返回,在\__trapret:中,根据switchk2u的内容完成对返回前的寄存器和栈的回复准备工作,最后通过iret指令,CPU返回“int T_SWITCH_TOU”的后一条指令处,以内核态模式继续执行。 23 | -------------------------------------------------------------------------------- /zh/chapter-3/figures/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-3/figures/1.png -------------------------------------------------------------------------------- /zh/chapter-3/figures/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-3/figures/10.png -------------------------------------------------------------------------------- /zh/chapter-3/figures/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-3/figures/11.png -------------------------------------------------------------------------------- /zh/chapter-3/figures/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-3/figures/2.png -------------------------------------------------------------------------------- /zh/chapter-3/figures/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-3/figures/3.png -------------------------------------------------------------------------------- /zh/chapter-3/figures/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-3/figures/4.png -------------------------------------------------------------------------------- /zh/chapter-3/figures/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-3/figures/5.png -------------------------------------------------------------------------------- /zh/chapter-3/figures/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-3/figures/6.png -------------------------------------------------------------------------------- /zh/chapter-3/figures/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-3/figures/7.png -------------------------------------------------------------------------------- /zh/chapter-3/figures/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-3/figures/8.png -------------------------------------------------------------------------------- /zh/chapter-3/figures/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-3/figures/9.png -------------------------------------------------------------------------------- /zh/chapter-3/handle_pages_fault.md: -------------------------------------------------------------------------------- 1 | # 【实现】缺页异常处理 2 | 3 | 当启动分页机制以后,如果一条指令或数据的虚拟地址所对应的物理页框不在内存中或者访问的类型有错误(比如写一个只读页或用户态程序访问内核态的数据等),就会发生缺页异常。产生页面异常的原因主要有: 4 | 5 | - 目标页面不存在(页表项全为0,即该线性地址与物理地址尚未建立映射或者已经撤销); 6 | - 相应的物理页面不在内存中(页表项非空,但Present标志位=0,比如在swap分区或磁盘文件上),这将在下面介绍换页机制实现时进一步讲解如何处理; 7 | - 访问权限不符合(此时页表项P标志=1,比如企图写只读页面). 8 | 9 | 当出现上面情况之一,那么就会产生页面page fault(#PF)异常。产生异常的线性地址存储在CR2中,并且将 #PF 的类型保存在 error code 中,比如 bit 0 表示是否 PTE_P为0,bit 1 表示是否 write 操作。 10 | 11 | 产生缺页异常后,CPU硬件和软件都会做一些事情来应对此事。首先缺页异常也是一种异常,所以针对一般异常的硬件处理操作是必须要做的,即CPU在当前内核栈保存当前被打断的程序现场,即依次压入当前被打断程序使用的eflags,cs,eip,errorCode;由于缺页异常的中断号是0xE, CPU把中断0xE服务例程的地址(vectors.S中的标号vector14处)加载到cs和eip寄存器中,开始执行中断服务例程。这时ucore开始处理异常中断,首先需要保存硬件没有保存的寄存器。在vectors.S中的标号vector14处先把中断号压入内核栈,然后再在trapentry.S中的标号\__alltraps处把ds、es和其他通用寄存器都压栈。自此,被打断的程序现场被保存在内核栈中。 12 | 13 | 接下来,在trap.c的trap函数开始了中断服务例程的处理流程,大致调用关系为: 14 | 15 | trap--> trap_dispatch-->pgfault_handler-->do_pgfault 16 | 17 | 下面需要具体分析一下do_pgfault函数。CPU把引起缺页异常的虚拟地址装到寄存器CR2中,并给出了出错码(tf->tf_err),指示引起缺页异常的存储器访问的类型。而中断服务例程会调用缺页异常处理函数do_pgfault进行具体处理。缺页异常处理是实现按需分页、swap in/out和写时复制的关键之处,后面的小节将分别展开讲述。 18 | 19 | ucore中do_pgfault函数是完成缺页异常处理的主要函数,它根据从CPU的控制寄存器CR2中获取的缺页异常的虚拟地址以及根据 error code的错误类型来查找此虚拟地址是否在某个VMA的地址范围内以及是否满足正确的读写权限,如果在此范围内并且权限也正确,这认为这是一次合法访问,但没有建立虚实对应关系。所以需要分配一个空闲的内存页,并修改页表完成虚地址到物理地址的映射,刷新TLB,然后调用iret中断,返回到产生缺页异常的指令处重新执行此指令。如果该虚地址不再某VMA范围内,这认为是一次非法访问。 20 | 21 | **【注意】** 22 | 23 | 地址空间的管理由虚存管理和页表管理两部分组成。 虚存管理限制了(程序)地址空间的范围以及权限,而页表维护的是实际使用的地址空间以及权限,后者不能比前者有更大的范围或者权限,因为前者是实际管理页表的。比如权限,虚存管理可以规定地址空间的某个范围是可写的,但是页表中却可以标记是read-only的(比如 copy-on-write 的实现),这种冲突可以被内核(通过硬件异常)轻易的捕获到,并进行相应的处理。反过来,如果页表权限比虚存规定的权限更大,内核是没有办法发现这种冲突的。由于虚存管理的存在,内核才能方便的实现更复杂和丰富的操作,比如 share memory、swap 等。 在后续的实验中还会遇到虚存管理只维护用户地址空间(也就是 [USERBASE, USERTOP) 区间)的情况,因为内核地址空间包括虚存和页表都是固定的。 24 | -------------------------------------------------------------------------------- /zh/chapter-3/implement_copy_on_write.md: -------------------------------------------------------------------------------- 1 | # proj9.2:实现写时复制 2 | 3 | proj9.2实现了写时复制(Copy On Write,简称COW)的主要功能,为lab3高效地创建子进程打下了基础。COW有何作用?这里又不得不提前讲讲lab3中的子进程创建。不同的进程应该具有不同的物理内存空间,当用户态进程发出fork( )系统调用来创建子进程时,ucore可复制当前进程(父进程)的整个地址空间,这样就有两块不同的物理地址空间了,新复制的那一块物理地址空间分配给子进程。这种行为是非常耗时和占内存资源的,因为它需要为子进程的页表分配页面,复制父进程的每一个物理内存页。如果子进程加载一个新的程序开始执行(这个过程会释放掉原来申请的全部内存和资源),这样前面的复制工作就白做了,完全没有必要。 4 | 5 | 为了解决上述问题,ucore采用一种有效的COW机制。其设计思想相对简单:父进程和子进程之间共享(share)页面而不是复制(copy)页面。但只要页面被共享,它们就不能被修改,即是只读的。注意此共享是指父子进程共享一个表示内存空间的mm_struct结构的变量。当父进程或子进程试图写一个共享的页面,就产生一个页访问异常,这时内核就把这个页复制到一个新的页面中并标记为可写。注意,原来的页面仍然是写保护的。当其它进程试图写入时,ucore检查写进程是否是这个页面的唯一属主(通过判断page_ref 和 swap_page_count 即 mem_map 中相关 entry 保存的值的和是否为1。注意区分与share memory的差别,share memory 通过 vma 中的 shmem 实现,这样的 page 是直接标记为共享的,而不是 copy on write,所以也没有任何冲突);如果是,它把这个页面标记为对这个进程是可写的。 6 | 7 | 在具体实现上,ucore调用dup_mmap函数,并进一步调用copy_range函数来具体完成对页表内容的复制,这样两个页表表示同一个虚拟地址空间(包括对应的物理地址空间),且还需修改两个页表中每一个页对应的页表项属性为只读,但。在这种情况下,两个进程有两个页表,但这两个页表只映射了一块只读的物理内存。同理,对于换出的页,也采用同样的办法来共享一个换出页。综上所述,我们可以总结出:如果一个页的PTE属性是只读的,但此页所属的VMA描述指出其虚地址空间是可写的,则这样的页是COW页。 8 | 9 | 当对这样的地址空间进行写操作的时候,会触发do_pgfault函数被调用。此函数如果发现是COW页,就会调用alloc_page函数新分配一个物理页,并调用memcpy函数把旧页的内容复制到新页中,并最后调用page_insert函数给当前产生缺页错的进程建立虚拟页地址到新物理页地址的映射关系(即改写PTE,并设置此页为可读写)。 10 | 11 | 这里还有一个特殊情况,如果产生访问异常的页已经被换出到硬盘上了,则需要把此页通过swap_in_page函数换入到内存中来,如果进一步发现换入的页是一个COW页,则把其属性设置为只读,然后异常处理结束返回。但这样重新执行产生异常的写操作,又会触发一次内存访问异常,则又要执行上一段描述的过程了。 12 | 13 | Page结构的ref域用于跟踪共享相应页面的进程数目。只要进程释放一个页面或者在它上面执行写时复制,它的ref域就递减;只有当ref变为0时,这个页面才被释放。 -------------------------------------------------------------------------------- /zh/chapter-3/pages_algors.md: -------------------------------------------------------------------------------- 1 | # 【原理】页内存分配算法 2 | 3 | 在proj5中进行在动态分配内存时,存在很多限制,效率很低。在操作系统原理中,为了有效地分配内存,首先需要了解和跟踪空闲内存和分布情况,一般可采用位图(bit map)和双向链表两种方式跟踪内存使用情况。若采用位图方式,则每个页对应位图区域的一个bit,如果此位为0,表示空闲,如果为1,表示被占用。采用位图方式很省空间,但查找n个长度为0的位串的开销比较大。而双向链表在查询或修改操作方面灵活性和效率较高,所以ucore采用双向链表来跟踪跟踪内存使用情况。 4 | 5 | 假设整个物理内存空闲空间的以页为单位被一个双向链表管理起来,每个表项管理一个物理页。这需要设计某种算法来查找空闲页和回收空闲页。ucore实现了首次适配(first fit)算法、最佳适配(best fit)算法、最差适配(worst fit)算法和兄弟(buddy)算法,这些算法都可以实现在ucore提供的物理内存页管理器框架pmm_manager下。 6 | 7 | 首次适配(first fit)算法的分配内存的设计思路是物理内存页管理器顺着双向链表进行搜索空闲内存区域,直到找到一个足够大的空闲区域,这是一种速度很快的算法,因为它尽可能少地搜索链表。如果空闲区域的大小和申请分配的大小正好一样,则把这个空闲区域分配出去,成功返回;否则将该空闲区分为两部分,一部分区域与申请分配的大小相等,把它分配出去,剩下的一部分区域形成新的空闲区。其释放内存的设计思路很简单,只需把这块区域重新放回双向链表中即可。 8 | 9 | 最佳适配(best fit)算法的设计思路是物理内存页管理器搜索整个双向链表(从开始到结束),找出能够满足申请分配的空间大小的最小空闲区域。找到这个区域后的处理以及释放内存的处理与上面类似。最佳适配算法试图找出最接近实际需要的空闲区,名字上听起来很好,其实在查询速度上较慢,且较易产生多的内存碎片。 10 | 11 | 最差适配(worst fit)算法与最佳适配(best fit)算法的设计思路相反,物理内存页管理器搜索整个双向链表,找出能够满足申请分配的空间大小的最大空闲区域,使新的空闲区比较大从而可以继续使用。在实际效果上,查询速度上也较慢,产生内存碎片相对少些。 12 | 13 | 上述三种算法在实际应用中都会产生碎片较多,效率不高的问题。为此一般操作系统会采用buddy算法来改进上述问题。buddy算法的基本设计思想是:在buddy系统中,被占用的内存空间和空闲内存空间的大小均为2的k次幂(k是正整数)。这样在ucore中,若申请n个页的内存空间,则实际可能分配的空间大小为2K个页(2k-1\1TB的硬盘,而且是非易失型的。 4 | 5 | 操作系统需要尽量满足编程人员的梦想,为此它需要管理上述存储器层次结构形成的存储空间,并完成如下主要任务: 6 | 7 | * 记录存储空间的使用情况,即记录哪些部分正在被使用,哪些部分还空闲; 8 | * 当需求方需要存储空间时,能快速地分配给它合适大小的空间;在需求方显式表示不需要申请到的存储空间时,能把存储空间回收,便于以后的分配; 9 | * 隔离不同的内存区域,确保在限制在一个内存区域中运行的软件无法访问区域以外的内存空间。这种机制称为地址保护(地址隔离)机制。 10 | * 如果内存太小,就需要把内存当中使用较少的数据所占空间送到磁盘上,给使用较多的数据腾出内存空间来;如果将来又访问到缓存到硬盘的数据,需要把这些数据重新加载到内存中进行访问。这种机制称为换入换出(swap in/out)机制,并涉及页替换算法。 11 | * 即使需求方表明了需要内存,但如果需求方没有实际访问所需内存前,则并不完成实际的物理内存分配。这种机制称为按需分配(如果是基于很分页机制,也称为按需分页)。 12 | * 设两个具有父子关系的程序共享同一地址空间(子程序同享父程序的地址空间),若二程序只是读此地址空间,则地址空间不会有变化;若其中一个程序对此地址空间某地址进行了写操作,则要把包含此地址的页空间复制一份给执行写操作的程序,这时此二程序将有不同的地址空间,可独立运行,相互不干扰。这种机制减少了父程序创建子程序地址空间的开销,称为写时复制(Copy On Write,简称COW)机制。 13 | 14 | 本章内容主要涉及操作系统的内存管理,包括物理内存管理和基于分页机制的虚拟内存管理。读者通过阅读本章的内容并动手实践相关的5个project实验: 15 | 16 | * proj5:能够探测物理内存并建立页表,实现分页管理 17 | * proj5.1/5.1.1/5.1.2:实现基于连续物理页的first/best/worst-fit分配算法 18 | * proj5.2:实现基于连续物理页的buddy分配算法 19 | * proj6:实现任意大小内存分配的slab分配算法 20 | * proj7:实现缺页中断服务例程和虚拟内存管理结构(VMM struct),提供按需分页的支持 21 | * proj8:实现类似改进时钟算法的页面置换算法并支持页粒度的换入换出机制 22 | * proj9/proj9.1/proj9.2:完善虚存管理(proj9)并逐步实现了进程间内存共享(proj9.1)和copy on write(COW)机制(proj9.2) 23 | 24 | **可以掌握如下知识:** 25 | * 与操作系统原理相关 26 | * 内存管理:基于分页机制的内存管理 27 | * 内存管理:连续内存分配算法 28 | * 内存管理:非连续内存分配算法 29 | * 内存管理和中断:缺页中断服务例程 30 | * 内存管理:虚存管理中的页面置换算法和页换入换出机制 31 | * 内存管理:按需分页机制和写时复制机制 32 | * 操作系统原理之外 33 | * 80386对分页管理(页表等)的硬件支持 34 | * 页粒度的页面置换策略和页换入换出的具体实现 35 | 36 | 本章内容中主要涉及内存管理的重要功能主要有两个: 37 | 38 | * 提供空闲内存空间:这样给操作系统和应用程序的代码和数据足够的存放“地方”,使得二者能够正常高效地运行,为此需要完成内存/外存的空间的分配、管理、释放等算法。 39 | * 提供内存空间隔离保护:隔离用户态应用程序和内核态操作系统之间,以及不同应用程序之间的内存空间,使得不会出现访问冲突,为此需要为不同的应用程序和操作系统划分不同的地址空间,一个应用程序越界规定的地址空间会出现内存访问故障中断。 40 | 41 | 为了让读者能够从实践上来理解内存管理的基本原理,我们设计了上述实验,主要的功能逐步实现如下所示: 42 | * 首先是扩展ucore的功能,使它能够发现并管理PC系统中可用的空闲物理内存; 43 | * 然后是建立分页机制,建立线性地址(分段机制已经完成了逻辑地址到线性地址的转换)到物理地址的映射关系和具体转换操作,这样使得应用程序无法直接访问到物理地址,而是只能访问由操作系统设定好的物理地址空间,从而使得应用程序的访问空间可控; 44 | * 为了高效地完成操作系统的其他功能单元和应用程序的空闲内存空间需求,需要设计以页(4096字节)为最小分配单位的面向连续物理地址空间的内存分配算法; 45 | 还要设计面向任意大小的内存空间(在物理地址空间上不一定连续)的虚拟内存分配算法; 46 | * 为了给应用程序提供超过实际物理内存空间大小的虚拟内存空间,需要把临时不常用到的内存换出(swap out)到硬盘(也称外存)中,等到需要访问的时候,再换入(swap in)到内存中。设计高效的页面置换算法会尽量保存经常访问的数据在内存中,而不经常访问的数据会换出到硬盘中。 47 | 48 | 49 | -------------------------------------------------------------------------------- /zh/chapter-3/virtual_mem_managment.md: -------------------------------------------------------------------------------- 1 | # 【原理】虚拟内存管理 2 | 3 | 什么是虚拟内存?简单地说,是指程序员或CPU “需要”和直接“看到”的内存,这其实暗示了两点:1、虚拟内存单元不一定有实际的物理内存单元对应,即实际的物理内存单元可能不存在;2、如果虚拟内存单元对应有实际的物理内存单元,那二者的地址一般不是相等的。通过操作系统的某种内存管理和映射技术可建立虚拟内存与实际的物理内存的对应关系,使得程序员或CPU访问的虚拟内存地址会转换为另外一个物理内存地址。 4 | 5 | 那么这个“虚拟”的作用或意义在哪里体现呢?在操作系统中,虚拟内存其实包含多个虚拟层次,在不同的层次体现了不同的作用。首先,在有了分段或分页机制后,程序员或CPU直接“看到”的地址已经不是实际的物理地址了,这已经有一层虚拟化,我们可简称为内存地址虚拟化。有了内存地址虚拟化,我们就可以通过设置段界限或页表项来设定软件运行时的访问空间,确保软件运行不越界,完成内存访问保护的功能。 6 | 7 | 通过内存地址虚拟化,可以使得软件在没有访问某虚拟内存地址时不分配具体的物理内存,而只有在实际访问某虚拟内存地址时,操作系统再动态地分配物理内存,建立虚拟内存到物理内存的页映射关系,这种技术属于lazy load技术,简称按需分页(demand paging)。把不经常访问的数据所占的内存空间临时写到硬盘上,这样可以腾出更多的空闲内存空间给经常访问的数据;当CPU访问到不经常访问的数据时,再把这些数据从硬盘读入到内存中,这种技术称为页换入换出(page swap in/out)。两个虚拟页的数据内容相同时,可只分配一个物理页框,这样如果对两个虚拟页的访问方式是只读方式,这这两个虚拟页可共享页框,节省内存空间;如果CPU对其中之一的虚拟页进行写操作,则这两个虚拟页的数据内容会不同,需要分配一个新的物理页框,并将物理页框标记为可写,这样两个虚拟页面将映射到不同的物理页帧,确保整个内存空间的正确访问。这种技术称为写时复制(Copy On Write,简称COW)。这三种内存管理技术给了程序员更大的内存“空间”,我们称为内存空间虚拟化。 8 | 9 | ucore在实现上述三种技术时,需要解决的一个关键问题是,何时进行请求调页/页换入换出/写时复制处理?其实,在程序的执行过程中由于某种原因(页框不存在/写只读页等)而使 CPU 无法最终访问到相应的物理内存单元,即无法完成从虚拟地址到物理地址映射时,CPU 会产生一次缺页异常,从而需要进行相应的缺页异常服务例程。这个缺页异常处理的时机就是求调页/页换入换出/写时复制处理的执行时机,当相关处理完成后,缺页异常服务例程会返回到产生异常的指令处重新执行,使得软件可以继续正常运行下去。 -------------------------------------------------------------------------------- /zh/chapter-3/vma_struct.md: -------------------------------------------------------------------------------- 1 | # 【实现】vma_struct数据结构和相关操作 2 | 3 | 在讲述缺页异常处理前,需要建立好虚拟内存空间描述。在proj7之前有关内存的数据结构和相关操作都是直接针对实际存在的资源--物理内存空间的管理,没有从一般应用程序对内存的“需求”考虑,即需要有相关的数据结构和操作来体现一般应用程序对虚拟内存的“需求”。一般应用程序的对虚拟内存的“需求”与物理内存空间的“供给”没有直接的对应关系,ucore是通过缺页异常处理来间接完成这二者之间的衔接。 4 | 5 | 在ucore中描述应用程序对虚拟内存“需求”的数据结构是vma_struct,以及针对vma_struct的函数操作。这里把一个vma_struct结构的变量简称为vma变量。vma_struct的定义如下: 6 | 7 | struct vma_struct { 8 | struct mm_struct *vm_mm; 9 | uintptr_t vm_start; 10 | uintptr_t vm_end; 11 | uint32_t vm_flags; 12 | list_entry_t list_link; 13 | }; 14 | 15 | vm_start和vm_end描述了一个连续地址的虚拟内存空间的起始位置和结束位置,这两个值都应该是 PGSIZE 对齐的,而且描述的是一个合理的地址空间范围(即严格确保 vm_start < vm_end 的关系);list_link是一个双向链表,按照从小到大的顺序把一系列用vma_struct表示的虚拟内存空间链接起来,并且还要求这些链起来的 vma_struct 应该是不相交的,即vma之间的地址空间无交集;vm_flags表示了这个虚拟内存空间的属性,目前的属性包括: 16 | 17 | #define VM_READ 0x00000001 //只读 18 | #define VM_WRITE 0x00000002 //可读写 19 | #define VM_EXEC 0x00000004 //可执行 20 | 21 | 以后还会引入如 VM_STACK 的其它属性来支持动态扩展用户栈空间。 22 | 23 | vm_mm是一个指针,指向一个比vma_struct更高的抽象层次的数据结构mm_struct,这里把一个mm_struct结构的变量简称为mm变量。这个数据结构表示了包含所有虚拟内存空间的共同属性,具体定义如下 24 | 25 | struct mm_struct { 26 | list_entry_t mmap_list; 27 | struct vma_struct *mmap_cache; 28 | pde_t *pgdir; 29 | int map_count; 30 | }; 31 | 32 | mmap_list是双向链表头,链接了所有属于同一页目录表的虚拟内存空间,mmap_cache是指向当前正在使用的虚拟内存空间,由于操作系统执行的“局部性”原理,当前正在用到的虚拟内存空间在接下来的操作中可能还会用到,这时就不需要查链表,而是直接使用此指针就可找到下一次要用到的虚拟内存空间。由于 mmap_cache 的引入,使得 mm_struct 数据结构的查询加速 30% 以上。pgdir 所指向的就是 mm_struct 数据结构所维护的页表。通过访问pgdir可以查找某虚拟地址对应的页表项是否存在以及页表项的属性等。map_count记录 mmap_list 里面链接的 vma_struct 的个数。 33 | 34 | 涉及mm_struct的操作函数比较简单,只有mm_create和mm_destroy两个函数,从字面意思就可以看出是是完成mm_struct结构的变量创建和删除。在mm_create中用kmalloc分配了一块空间,所以在mm_destroy中也要对应进行释放。在ucore运行过程中,会产生描述虚拟内存空间的vma_struct结构,所以在mm_destroy中也要进对这些mmap_list中的vma进行释放。涉及vma_struct的操作函数也比较简单,主要包括三个: 35 | 36 | - vma\_create--创建vma 37 | - insert\_vma\_struct--插入一个vma 38 | - find\_vma--查询vma。 39 | 40 | vma\_create函数根据输入参数vm\_start、vm\_end、vm\_flags来创建并初始化描述一个虚拟内存空间的vma_struct结构变量。insert_vma_struct函数完成把一个vma变量按照其空间位置[vma->vm_start,vma->vm_end]从小到大的顺序插入到所属的mm变量中的mmap_list双向链表中。find_vma根据输入参数addr和mm变量,查找在mm变量中的mmap_list双向链表中某个vma包含此addr,即vma->vm_start<= addr end。这三个函数与后续讲到的缺页异常处理有紧密联系。 41 | -------------------------------------------------------------------------------- /zh/chapter-3/x86_pages_hardware.md: -------------------------------------------------------------------------------- 1 | # 【背景】X86的分页硬件支持 2 | 3 | X86 CPU对实际物理内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的,在前端总线上传输的内存地址是物理内存地址。物理内存地址被北桥映射到实际的内存条中的内存单元相应位置上。然而,在CPU内执行带来软件所使用的是虚拟内存地址(也称逻辑内存地址),它必须被转换成物理地址后,才能用于实际内存访问。 4 | 5 | 前面已经讲过了80x86的分段机制,80x86的分页机制建立在其分段机制基础之上,提供了更加强大的内存管理支持。需要注意的是,在x86中,必须先有分段机制,才能有分页机制。在分段机制中,虚地址会转换为线性地址。如果不启动分页机制,那么线性地址就是最终在前端总线上的物理地址;如果启动了分页机制,则线性地址还会经过页映射被转换为物理地址。 6 | 7 | 那如果启动分页机制呢?在80x86中有一个CR0控制寄存器,它包含一个PG位,如果PG=1,启用分页机制;如果 PG=0,禁用分页机制。不像分段机制管理大小不固定的内存卡,分页机制以固定大小的存储块为最小管理单位,即把整个地址空间(包括线性地址和物理地址)都看成由固定大小的存储块组成。在80x86中,这个固定大小一般设定为4096字节。在线性地址空间中的最小管理单位(称为页(page)),可以映射到物理地址空间中的任何一个最小管理单位(称为页帧(page frame))。页/页帧的32位地址由20位的页号/页帧号和12位的页/页帧内偏移组成。 8 | 9 | 80x86分页机制中的分页转换功能(即线性地址到物理地址的映射功能)需采用驻留在内存中的数组来描述,该数组称为页表(page table)。每个数组项就是一个页表项。由于页/页帧基地址按4096字节对齐,因此页/页帧的基地址的低12位是0。页地址<->页帧地址的转换过程以简单地看做80x86对页表的一个查找过程。页地址(线性地址)的高20位(即页号,or页的基地址)构成这个数组的索引值,用于选择对应页帧的页帧号(即页帧的基地址)。页地址的低12位给出了页内偏移量,加上对应的页帧基地址就最终形成对应的页帧地址(即物理地址)。 10 | 11 | 由于80x86的地址空间可达到4GB,按页大小(4KB)划分为1M个页。如果用一个页表来描述这种映射,那么该也表就要有1M个表项,若每个表项占用4个字节,那么该映射表就要占用4M字节。考虑到将来一个进程就需要一个地址映射表,若有多个进程,那地址映射表所占的总空间将非常巨大。为避免地址映射表占用过多的内存资源,80x86把地址映射表设定为两级。地址映射表的第一级称为页目录表,存储在一个4KB的物理页中,页目录表共有1K个表项,其中每个表项为4字节长,页表项中包含对应第二级表所在的基地址。地址映射表的第二级称为页表,每个页表也安排在一个4K字节的页中,每张页表中有1K个表项,每个表项为4字节长,包含对应页帧的基地址。由于页目录表和页表均由1K个表项组成,所以使用10位的索引就能指定表项,即用10位的索引值乘以4加基地址就得到了表项的物理地址。按上述的地址转换描述,一个页表项只需20位,但实际的页表项是32位,那其他的12位有何用途呢? 12 | 13 | 在80x86中的的页目录表项结构定义如下所示: 14 | 15 | ![1](figures/1.png) 16 | 17 | 在80x86中的的页表项结构定义如下所示: 18 | 19 | ![2](figures/2.png) 20 | 21 | 其中低12位的相应属性位含义如下: 22 | 23 | * P位:存在(Present)标志,用于指明此表项是否有效。P=1表示有效;P=0表示无效。如果80x86访问一个无效的表项,则会产生一个异常。如果P=0,那么除表示表项无效外,其余位用于其他用途(比如swap in/out中,用来保存已存储在磁盘上的页面的序号)。 24 | * R/W:读/写(Read/Write)标志,如果R/W=1,表示页的内容可以被读、写或执行。如果R/W=0,表示页的内容只读或可执行。当处理器运行在特权级(级别0、1或2)时,则R/W位不起作用。 25 | * U/S:是用户态/特权态(User/Supervisor)标志。如果U/S=1,那么在用户态和特权态都可以访问该页。如果U/S=0,那么只能在特权态(0、1或2)可访问该页。 26 | * A:是已访问(Accessed)标志。当CPU访问页表项映射的物理页时,页表项的这个标志就会被置为1。可通过软件把该标志位清零,并且操作系统可通过该标志来统计页的使用情况,用于页替换策略。 27 | * D:是页面已被修改(Dirty)标志。当CPU写页表项映射的物理页内容时,页表项的这个标志就会被置为1。可通过软件把该标志位清零,并且操作系统可通过该标志来统计页的修改情况,用于页替换策略。 28 | 29 | 下图显示了由页目录表和页表构成的二级页表映射架构。 30 | 31 | ![3](figures/3.png) 32 | 图 页目录表和页表构成的二级页表映射架构 33 | 34 | 从图中可见,控制寄存器CR3的内容是对应页目录表的物理基地址;页目录表可以指定1K个页表,这些页表可以分散存放在任意的物理页中,而不需要连续存放;每张页表可以指定1K个任意物理地址空间的页。存储页目录表和页表的基地址是按4KB对齐。当采用上述页表结构后,基于分页的线性地址到物理地址的转换过程如下图所示: 35 | 36 | 首先,CPU把控制寄存器CR3的高20位作为页目录表所在物理页的物理基地址,再把需要进行地址转换的线性地址的最高10位(即22~ 31位)作为页目录表的索引,查找到对应的页目录表项,这个表项中所包含的高20位是对应的页表所在物理页的物理基地址;然后,再把线性地址的中间10位(即12~21位)作为页表中的页表项索引,查找到对应的页表项,这个表项所包含的的高20位作为线性地址的基地址(即页号)对应的物理地址的基地址(即页帧号);最后,把页帧号作为32位物理地址的高20位,把线性地址的低12位不加改变地作为32位物理地址的低12位,形成最终的物理地址。 37 | 38 | 如果每次访问内存单元都要访问位于内存中的页表,则访存开销太大。为了避免这类开销,x86 CPU把最近使用的地址映射数据存储在其内部的页转换高速缓存(页转换查找缓存,简称TLB)中。这样在访问存储器页表之前总是先查阅高速缓存,仅当必须的转换不在高速缓存中时,才访问存储器中的两级页表。 -------------------------------------------------------------------------------- /zh/chapter-4/create_user_thread.md: -------------------------------------------------------------------------------- 1 | # 创建并执行用户线程 2 | 3 | ## 实验目标 4 | 5 | 到proj12为止,ucore还一直没有用户线程。而用户线程与用户进程的区别在于操作系统用户进程管理除了涉及与执行过程相关的调度、进程上下文切换、执行状态变化外,还需管理内存、文件等资源,而用户线程只管理与执行过程相关的调度、上下文切换、执行状态变化。这样使得在执行线程创建、删除和线程上下文切换时的开销比对进程做类似的事情要少很大的开销。从而在操作系统的用户线程管理下,可以让应用程序开发员开发多线程应用软件的执行效率更高。 6 | 7 | 为了支持用户线程,我们还需对现有的进程管理进行有限扩展,在了解线程基本原理的情况下,设计并实现ucore对用户线程的基本支持。 8 | 9 | ## 概述 10 | 11 | ### 实现描述 12 | 13 | proj12是lab3的最后一个project。它在proj10.1(中间还有proj10.2/10.3/10.4/11)的基础上实现了对用户线程的支持,主要参考了Linux的线程实现思路,把线程作为一个共享内存等资源的轻量级进程看待,扩展设计了进程控制块中支持用户线程的成员变量和与进程管理相关的系统调用,使得在现有进程管理的基础上,做相对较小的改动,就支持线程模型了。 14 | 15 | ### 项目组成 16 | 17 | proj12 18 | ├── … 19 | │   ├── process 20 | │   │   ├── proc.c 21 | │   │   ├── proc.h 22 | │   │   └── … 23 | │   ├── syscall 24 | │   │   ├── syscall.c 25 | │   │   └── … 26 | └── user 27 | ├── libs 28 | │   ├── clone.S 29 | │   ├── lock.h 30 | │   ├── thread.c 31 | │   ├── thread.h 32 | │   ├── ulib.c 33 | │   └── ulib.h 34 | ├── threadfork.c 35 | ├── threadtest.c 36 | ├── threadwork.c 37 | └── … 38 | 39 | 相对于proj11,主要增加和扩展的文件如下: 40 | 41 | * kern/proc.[ch]:扩展进程控制块,增加线程组成员变量,并扩展和增加与线程管理相关的函数; 42 | * kern/syscall.c:增加用于线程创建的sys_clone系统调用; 43 | * user/libs/*.[chS]:实现用户态创建线程的库调用函数、访问系统调用函数; 44 | * user/thread*.c:线程支持用户测试用例。 45 | 46 | ### 编译运行 47 | 48 | 首先确保proj12的kern/proc.c中的user_main函数中的代码为: 49 | 50 | static int user_main(void *arg) { #ifdef TEST     KERNEL_EXECVE2(TEST, TESTSTART, TESTSIZE); #else     KERNEL_EXECVE(threadtest); #endif     panic("user_main execve failed.\n"); } 51 | 52 | 53 | 编译并运行proj12的命令如下: 54 | 55 | make 56 | make qemu 57 | 58 | 则可以得到如下显示界面: 59 | 60 | thuos:~/oscourse/ucore/i386/lab3_process/proj12$ make qemu 61 | (THU.CST) os is loading ... 62 | …… 63 | ++ setup timer interrupts 64 | kernel_execve: pid = 3, name = "threadtest". 65 | thread ok. 66 | child ok. 67 | threadtest pass. 68 | all user-mode processes have quit. 69 | init check memory pass. 70 | kernel panic at kern/process/proc.c:454: 71 | initproc exit. 72 | 73 | Welcome to the kernel debug monitor!! 74 | Type 'help' for a list of commands. 75 | K> 76 | 77 | 这其实是ucore首先创建了一个用户进程usermain,然后此用户进程通过调用sys_execv执行的是users/threadtest.c中的代码: 78 | 79 | #include  80 | #include  81 | #include  82 | int test(void *arg) { 83 | cprintf("child ok.\n"); 84 | return 0xbee; 85 | } 86 | int main(void) { 87 | thread_t tid; 88 | assert(thread(test, NULL, &tid) == 0);     89 | cprintf("thread ok.\n");      90 | int exit_code;      91 | assert(thread_wait(&tid, &exit_code) == 0 && exit_code == 0xbee);      92 | cprintf("threadtest pass.\n");      93 | return 0; 94 | } 95 | 96 | usermain用户进程调用了thread用户库函数来创建了一个用户线程test,这个用户线程同享了usermain用户进程的地址空间,然后usermain用户进程就调用thread_wait函数等待用户线程结束。用户线程test在结束执行时,会设置退出码为0xbee。用户进程usermain会检查此退出码,看用户线程是否结束。上述执行过程其实包含了对用户线程整个生命周期的管理。下面我们将从原理和实现两个方面对此进行进一步阐述。 97 | 98 | -------------------------------------------------------------------------------- /zh/chapter-4/figures/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-4/figures/0.png -------------------------------------------------------------------------------- /zh/chapter-4/figures/0_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-4/figures/0_2.png -------------------------------------------------------------------------------- /zh/chapter-4/figures/0_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-4/figures/0_3.png -------------------------------------------------------------------------------- /zh/chapter-4/figures/0_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-4/figures/0_4.png -------------------------------------------------------------------------------- /zh/chapter-4/figures/0_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-4/figures/0_5.png -------------------------------------------------------------------------------- /zh/chapter-4/figures/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-4/figures/1.png -------------------------------------------------------------------------------- /zh/chapter-4/figures/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-4/figures/2.png -------------------------------------------------------------------------------- /zh/chapter-4/figures/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-4/figures/3.png -------------------------------------------------------------------------------- /zh/chapter-4/figures/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-4/figures/4.png -------------------------------------------------------------------------------- /zh/chapter-4/process_exit_wait.md: -------------------------------------------------------------------------------- 1 | # 进程退出和等待进程 2 | 3 | 当进程执行完它的工作后,就需要执行退出操作,释放进程占用的资源。Ucore分了两步来完成这个工作,首先由进程本身完成大部分资源的占用内存回收工作,然后由此进程的父进程完成剩余资源占用内存的回收工作。为何不让进程本身完成所有的资源回收工作呢?这是因为进程要执行回收操作,就表明此进程还存在,还在执行指令,这就需要内核栈的空间不能释放,且表示进程存在的进程控制块不能释放。所以需要父进程来帮忙释放子进程无法完成的这两个资源回收工作。 4 | 5 | 为此在用户态的函数库中提供了exit函数,此函数最终访问sys_exit系统调用接口让操作系统来帮助当前进程执行退出过程中的部分资源回收。我们来看看ucore是如何做进程退出工作的。需要注意,这部分实现在proj10.2中才完成。所以我们这里是基于proj10.2的代码来进行分析。 6 | 7 | 首先,exit函数会把一个退出码error_code传递给ucore,ucore通过执行内核函数do_exit来完成对当前进程的退出处理,主要工作简单地说就是回收当前进程所占的大部分内存资源,并通知父进程完成最后的回收工作,具体流程如下: 8 | 9 | 1. 如果current->mm != NULL,表示是用户进程,则开始回收此用户进程所占用的用户态虚拟内存空间; 10 | 11 | a. 首先执行“lcr3(boot_cr3)”,切换到内核态的页表上,这样当前用户进程目前只能在内核虚拟地址空间执行了,这是为了确保后续释放用户态内存和进程页表的工作能够正常执行; 12 | 13 | b. 如果当前进程控制块的成员变量mm的成员变量mm_count减1后为0(表明这个mm没有再被其他进程共享,可以彻底释放进程所占的用户虚拟空间了。),则开始回收用户进程所占的内存资源: 14 | 15 | * 调用exit_mmap函数释放current->mm->vma链表中每个vma描述的进程合法空间中实际分配的内存,然后把对应的页表项内容清空,最后还把页表所占用的空间释放并把对应的页目录表项清空; 16 | * 调用put_pgdir函数释放当前进程的页目录所占的内存; 17 | * 调用mm_destroy函数释放mm中的vma所占内存,最后释放mm所占内存; 18 | 19 | c. 此时设置current->mm为NULL,表示与当前进程相关的用户虚拟内存空间和对应的内存管理成员变量所占的内核虚拟内存空间已经回收完毕; 20 | 21 | 2. 这时,设置当前进程的执行状态current->state=PROC_ZOMBIE,当前进程的退出码current->exit_code=error_code。此时当前进程已经不能被调度了,需要此进程的父进程来做最后的回收工作(即回收描述此进程的内核栈和进程控制块); 22 | 23 | 3. 如果当前进程的父进程current->parent处于等待子进程状态(即current->parent->wait_state==WT_CHILD),则唤醒父进程(即执行“wakup_proc(current->parent)”),让父进程帮助自己完成最后的资源回收工作; 24 | 25 | 4. 如果当前进程还有子进程,则需要把这些子进程的父进程指针设置为内核线程initproc,且各个子进程指针需要插入到initproc的子进程链表中。如果某个子进程的执行状态是PROC_ZOMBIE,则需要唤醒initproc来完成对此子进程的最后回收工作。 26 | 27 | 5. 执行schedule()函数,选择新的进程执行。 28 | 29 | 那么父进程如何完成对子进程的最后回收工作呢?这要求父进程要执行wait用户函数或wait_pid用户函数,这两个函数的区别是,wait函数等待任意子进程的结束通知,而wait_pid函数等待进程id号为pid的子进程结束通知。这两个函数最终访问sys_wait系统调用接口让ucore来完成对子进程的最后回收工作,即回收子进程的内核栈和进程控制块所占内存空间,具体流程如下: 30 | 31 | 1. 如果pid!=0,表示只找一个进程id号为pid的退出状态的子进程,否则找任意一个处于退出状态的子进程; 32 | 2. 如果此子进程的执行状态不为PROC_ZOMBIE,表明此子进程还没有退出,则当前进程只好设置自己的执行状态为PROC_SLEEPING,睡眠原因为WT_CHILD(即等待子进程退出),调用schedule()函数选择新的进程执行,自己睡眠等待,如果被唤醒,则重复跳回步骤1处执行; 33 | 3. 如果此子进程的执行状态为PROC_ZOMBIE,表明此子进程处于退出状态,需要当前进程(即子进程的父进程)完成对子进程的最终回收工作,即首先把子进程控制块从两个进程队列proc_list和hash_list中删除,并释放子进程的内核堆栈和进程控制块。自此,子进程才彻底地结束了它的执行过程,消除了它所占用的所有资源。 34 | 35 | **【问题】哪些资源是子进程无法回收,需要父进程帮忙回收的?** 36 | 37 | **【问题】当子进程执行了sys_exit系统调用,但父进程还没有执行sys_wait系统调用的情况下,子进程还能正常通知父进程让父进程回收子进程最后无法回收的子进程所占资源吗?** 38 | -------------------------------------------------------------------------------- /zh/chapter-4/process_managment.md: -------------------------------------------------------------------------------- 1 | # 进程管理与调度 2 | 3 | “进程”(process)是20世纪60年代初首先由MIT的MULTICS系统和IBM公司的CTSS/360系统率先引入的概念。简单地说,进程是一个正在运行的程序。但传统的程序本身是一组指令的集合,是一个静态的概念。程序在一方面无法描述程序在内存中的动态执行情况,即无法从程序的字面上看出它何时执行,何时结束;另一方面,在内存中可存在多个程序,这些程序分时复用一个CPU,但无法清楚地表达程序间关系(比如父子关系、同步互斥关系等)。因此,程序这个静态概念已不能如实反映多程序并发执行过程的特征。 4 | 5 | 为了从根本上描述程序动态执行过程的性质,计算机科学家引入了“进程(Process)”概念。在计算机系统中,由于CPU的速度非常快(现在的通用CPU主频达到2GHz是很平常的事情),只让它做一件事情无法充分发挥其能力。我们可以“同时”运行多个程序,这个“同时”,其实是操作系统给用户造成的一个“错觉”。大家知道,CPU是计算机系统中的硬件资源。为了提高CPU的利用率,在内存中的多个程序可分时复用CPU,即如果一个程序因某个事件而不能继续执行时,就可把CPU占用权转交给另一个可运行程序。为了刻画多各程序的并发执行的过程,就引入了“进程”的概念。从操作系统原理上看,一个进程是一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程。操作系统中的进程管理需要协调多道程序之间的关系,解决对处理器分配调度策略、分配实施和回收等问题,从而使得处理器资源得到最充分的利用。 6 | 7 | 操作系统需要管理这些进程,使得这些进程能够公平、合理、高效地分时使用CPU,这需要操作系统完成如下主要任务: 8 | 9 | - 进程生命周期管理:创建进程、让进程占用CPU执行、让进程放弃CPU执行、销毁进程; 10 | - 进程分派(dispatch)与调度(scheduling):设定进程占用/放弃CPU的时机、根据某种策略和算法选择将占用的CPU(这就是调度),完成进程切换; 11 | - 进程内存空间保护:给每个进程一个受到保护的地址空间,确保进程有独立的地址空间,不会被其他进程非法访问; 12 | - 进程内存空间等资源共享:提供内存等资源共享机制,可以使得不同进程可共享内存等资源; 13 | - 系统调用机制:给用户进程提供访问操作系统功能的接口,即系统调用接口。 14 | 15 | 本章内容主要涉及操作系统的进程管理与调度,并能够利用lab2的虚存管理功能实现高效的进程中的内存管理。读者通过阅读本章的内容并动手实践相关的lab3和lab4种的9个project实验: 16 | 17 | - Proj10:创建进程控制块和内核线程。 18 | - Proj10.1:实现用户进程、读和加载ELF格式执行程序、一个简单的调度器,以及提供创建(fork)/execve(执行)/ 放弃对CPU的占用(yield)等系统调用实现。 19 | - Proj10.2:完成等待子进程结束(wait),杀死进程(kill),进程自己退出(exit)等系统调用, 从而完善了进程的生命周期管理。 20 | - Proj10.3:实现sys\_brk系统调用和相应的用户进程内存管理,从而与lab2的内存管理进一步联合在一起。 21 | - Proj10.4:让进程可以睡眠和被唤醒。 22 | - Proj11:创建kswapd内核线程来专门处理内存页的换入和换出。 23 | - Proj12:基于进程间内存共享(proj9.1)实现父子进程数据共享,并实现了用户态的线程机制。 24 | - Proj13:设计实现了通用的调度器框架 25 | - Proj13.1/Proj13.2:在通用调度器框架下实现了轮转(RoundRobin, RR)调度器/多级反馈队列(Multi Level Feedback Queue, MLFQ)调度器 26 | 27 | 可以掌握如下知识: 28 | 29 | - 与操作系统原理相关 30 | - 进程管理:进程状态和进程状态转换 31 | - 进程管理:进程创建、进程删除、进程阻塞、进程唤醒 32 | - 进程管理:父子进程的关系和区别 33 | - 进程管理:进程中的内存管理 34 | - 进程管理:用户进程、内核进程、用户线程、内核线程的关系区别 35 | - 进程管理:线程的特征和实现机制 36 | - 进程调度:进程调度算法 37 | - 操作系统原理之外 38 | - 页面换入换出的内核线程实现技术 39 | - 父子进程数据共享实现 40 | - 通用的调度器框架 41 | - 进程切换的实现细节 42 | 43 | 本章内容中主要涉及进程管理的重要功能主要有两个: 44 | 45 | - 进程生命周期的管理:如何高效地创建进程、切换进程、删除进程和管理进程对资源的需求(内存和CPU)涉及一系列的动态管理机制。线程的加入使得整个系统的执行效率更高,这需要根据线程的特点设计与进程不同的线程组织和管理机制。 46 | - 进程调度算法:进程调度(部分教科书也称为处理器调度)算法主要是选择响应时间决定应该由哪个进程占用CPU来执行。这里需要确保通过调度来提高整个系统的吞吐量和减少响应时间。 47 | 48 | 为了让读者能够从实践上来理解进程管理和调度的基本原理,我们设计了上述实验,主要的功能逐步实现如下所示: 49 | 50 | - 首先需要能够对运行的程序进行管理,这需要一个“档案”,进程控制块(Process Control Block); 51 | - 有了进程控制块,我们就可以实现不同特点的进程或线程,这里首先实现了相对简单的内核线程; 52 | - 为了能够执行应用程序,还需通过进程控制块实现用户进程的管理,能够创建/删除/阻塞/唤醒进程,从而能够对用户进程的整个生命周期进行全程管理; 53 | - 由于在内存中有多个进程,但只有一个CPU,所以需要设计合理的调度器,让不同进程能够分时复用CPU,从而提高整个系统的吞吐量和减少响应时间。 54 | 55 | 56 | -------------------------------------------------------------------------------- /zh/chapter-4/process_schedule.md: -------------------------------------------------------------------------------- 1 | #进程调度 2 | 3 | ## 实验目标 4 | 5 | 在只有一个或几个CPU的计算机系统中,进程数量一般远大于CPU数量,CPU是一个稀缺资源,多个进程不得不分时占用CPU执行各自的工作,操作系统必须提供一种手段来保证各个进程“和谐”地共享CPU。为此,需要在lab3的基础上设计实现ucore进程调度类框架以便于设计各种进程调度算法,同时以提高计算机整体效率和尽量保证各个进程“公平”地执行作为目标来设计各种进程调度算法。 6 | 7 | ## proj13/13.1/13.2概述 8 | 9 | ### 实现描述 10 | 11 | project13是lab4的第一个项目,它基于lab3的最后一个项目proj12。主要参考了Linux-2.6.23设计了一个简化的进程调度类框架,能够在此框架下实现不同的调度算法,并在此框架下实现了一个简单的FIFO调度算法。接下来的proj13.1在此调度框架下实现了轮转(Round Robin,简称RR)调度算法,proj13.2在此调度框架下实现了多级反馈队列(Multi-Level Feed Back,简称MLFB)调度算法。 12 | 13 | ### 项目组成 14 | 15 | proj13 16 | ├── kern 17 | │   ├── ...... 18 | │   ├── process 19 | │   │   ├──…… 20 | │   │   ├── proc.h 21 | │   │   └── proc.c 22 | │   ├── schedule 23 | │   │   ├── sched.c 24 | │   │   ├── sched_FCFS.c 25 | │   │   ├── sched_FCFS.h 26 | │   │   └── sched.h 27 | │   └── trap 28 | │   ├── trap.c 29 | │   └── …… 30 | └── user 31 | ├── matrix.c 32 | └── …… 33 | 34 | 17 directories, 129 files 35 | 36 | 相对与proj12,proj13增加了3个文件,修改了相对重要的5个文件。主要修改和增加的文件如下: 37 | 38 | * process/proc.[ch]:扩展了进程控制块的定义能够把处于就绪态的进程放入到一个就绪队列中,并在创建进程时对新扩展的成员变量进行初始化。 39 | * schedule/sched.[ch]:增加进程调度类的定义和相关进程调度类的共性函数。 40 | * schedule/sched_FCFS.[ch]:基于进程调度类的FCFS调度算法实例的设计实现; 41 | * schedule/sched.[ch]:实现了一个先来先服务(First Come First Serve)策略的进程调度。 42 | * user/matrix.c:矩阵乘用户测试程序 43 | 44 | ### 编译运行 45 | 46 | 编译并运行proj13的命令如下: 47 | 48 | make 49 | make qemu 50 | 51 | 则可以得到如下显示界面 52 | 53 | (THU.CST) os is loading ... 54 | 55 | Special kernel symbols: 56 | …… 57 | ++ setup timer interrupts 58 | kernel_execve: pid = 3, name = "matrix". 59 | fork ok. 60 | pid 4 is running (1000 times)!. 61 | pid 4 done!. 62 | pid 5 is running (1400 times)!. 63 | pid 5 done!. 64 | …… 65 | pid 22 is running (33400 times)!. 66 | pid 22 done!. 67 | pid 23 is running (33400 times)!. 68 | pid 23 done!. 69 | matrix pass. 70 | all user-mode processes have quit. 71 | init check memory pass. 72 | kernel panic at kern/process/proc.c:456: 73 | initproc exit. 74 | 75 | Welcome to the kernel debug monitor!! 76 | Type 'help' for a list of commands. 77 | K> 78 | 79 | 这其实是在采用简单的FCFS调度方法来执行matrix用户进程,这个matrix进程将创建20个进程来各自执行二维矩阵乘的工作。Ucore将按照FCFS的调度方法,一个一个地按创建顺序执行每个子进程。一个子进程结束后,再调度另外一个子进程运行。这实际上看不出ucore是如何具体实现进程调度类框架和FCFS调度算法的。下面我们首先介绍一下进程调度的基本原理,然后再分析ucore的调度框架和调度算法的实现。 80 | -------------------------------------------------------------------------------- /zh/chapter-4/process_status_change.md: -------------------------------------------------------------------------------- 1 | # 进程运行状态转变过程 2 | 3 | 分析完从进程/线程从创建到退出的整个过程,我们需要在从全局的角度来看看进程/线程在做整个运行过程中的运行状态转变过程。在执行状态转变过程中,ucore在调度过程总,并没有区分线程和进程,所以进程和线程的执行状态转变是一致的,分析的结果适合用户线程和用户进程的执行过程。 4 | 5 | 首先为了描述进程/线程的整个状态集合,ucore在kern/process/proc.h中定义了进程/线程的运行状态: 6 | 7 | // process's state in his life cycle 8 | enum proc_state { 9 | PROC_UNINIT = 0,  // uninitialized 10 | PROC_SLEEPING,    // sleeping 11 | PROC_RUNNABLE,    // runnable(maybe running) 12 | PROC_ZOMBIE,      // almost dead, and wait parent proc to reclaim his resource 13 | }; 14 | 15 | 这与操作系统原理讲解的五进程执行状态相比,少了一个PROC_RUNNING态(表示正在占用CPU执行),这是由于在ucore中,用current(基于proc_strcut数据结构)进程控制块指针指向了当前正在运行的进程/线程PROC_RUNNING态,所以就没必要再增加一个PROC_RUNNING态了。那么那些事件或内核函数会触发状态的转变呢?通过分析uore源码,我们可以得到如下表示: 16 | 17 | ![0_5](figures/0_4.png) 18 | 19 | 当父进程得到子进程的通知,回收完子进程控制块所占内存后,这个进程就彻底消失了。我们也可以用一个类似有限状态自动机来表示状态的变化:`(需要用visio重画)` 20 | 21 | 22 | process state changing: 23 | 24 | alloc_proc                                 RUNNING 25 | +                                   +--<----<--+ 26 | +                                   + proc_run + 27 | V                                   +-->---->--+  28 | PROC_UNINIT -- proc_init/wakeup_proc --> PROC_RUNNABLE -- try_free_pages/do_wait/do_sleep --> PROC_SLEEPING -- 29 | A      +                                                           + 30 | |      +--- do_exit --> PROC_ZOMBIE                                + 31 | +                                                                  +  32 | -----------------------wakeup_proc---------------------------------- 33 | 34 | -------------------------------------------------------------------------------- /zh/chapter-4/process_summary.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/chapter-4/process_summary.md -------------------------------------------------------------------------------- /zh/chapter-4/wait_awaken_based_time_event.md: -------------------------------------------------------------------------------- 1 | # 基于时间事件的等待与唤醒 2 | 3 | Clock(时钟)中断(irq0,可回顾第二章2.4节) 可给操作系统提供有一定间隔的时间事件, 操作系统将其作为基本的计时单位,这里把两次时钟中断之间的时间间隔为一个时间片(timer splice)。基于此时间片,操作系统得以向上提供基于时间点的事件,并实现基于固定时间长度的等待和唤醒机制。在每个时钟中断发生时,操作系统可产生对应时间长度的时间事件,这样操作系统和应用程序可基于这些时间事件来构建基于时间的软件设计。在proj10.4中,实现了定时器timer的支持。 4 | 5 | ## timer数据结构 6 | 7 | sched.h定义了有关timer数据结构, 8 | 9 | typedef struct { 10 | unsigned int expires; //到期时间 11 | struct proc_struct *proc; //等待时间到期的进程 12 | list_entry_t timer_link; //链接到timer_list的链表项指针 13 | } timer_t; 14 | 15 | 这几个成员变量描述了一个timer(定时器)涉及的相关因素,首先一个expires表明了这个定时器何时到期,而第二个成员变量描述了定时器到期后需要唤醒的进程,最后一个参数是一个链表项,用于把自身挂到系统的timer链表上,以便于扫描查找特定的timer。 16 | 17 | ## timer相关操作 18 | 19 | 一个 timer在ucore中的生存周期可以被描述如下: 20 | 21 | 1. 某进程创建和初始化timer_t结构的一个timer,并把timer被加入系统timer管理列表timer_list中,进程设置为基于timer事件的阻塞态(即睡眠了),这样这个timer就诞生了,; 22 | 2. 系统时间通过时钟中断被不断累加,且ucore定期检查是否有某个timer的到期时间已经到了,如果没有到期,则ucore等待下一次检查,此timer会挂在timer_list上继续存在; 23 | 3. 如果到期了,则对应的进程又处于就绪态了,并从系统timer管理列表timer_list中移除该 timer,自此timer就死亡退出了。 24 | 25 | 基于上述timer生存周期的流程,与timer相关的函数如下: 26 | 27 | * timer init:对timer的成员变量进行初始化,设定了在expires时间之后唤醒proc进程 28 | * add_timer:向系统timer链表timer_list添加某个初始化过的timer,这样该timer计时器将在指定时间expires后被扫描到,如果等待则这个定时器timer的进程处在等待状态,并将将进程唤醒,进程将处于就绪态。 29 | * del_timer:向系统timer链表timer_list删除(或者说取消)某一个计时器。该计时 器在取消后,对应的进程不会被系统在指定时刻expires唤醒。 30 | * run_timer_list:被trap函数调用,遍历系统timer链表timer_list中的timer计时器,找出所有应该到时的timer计时器,并唤醒与此计时器相关的等待进程,再删除此timer计时器。在lab4/proj13以后,还增加了对进程调度器在时间事件产生后的处理函数的调用(在后续有进一步分析)。 31 | 32 | 有了这些函数的支持,我们就可以实现进程睡觉并被定时唤醒的功能了。比如ucore在用户函数库中提供了sleep函数,当用户进程调用sleep函数后,会进一步调用sys_sleep系统调用,在内核中完成sys_sleep系统调用服务的是do_sleep内核函数,其实现如下: 33 | 34 | int 35 | do_sleep(unsigned int time) { 36 | …… 37 | timer_t __timer, *timer = timer_init(&__timer, current, time); 38 | current->state = PROC_SLEEPING; 39 | current->wait_state = WT_TIMER; 40 | add_timer(timer); 41 | …… 42 | schedule(); 43 | del_timer(timer); 44 | return 0; 45 | } 46 | 47 | 可以看出,do_sleep首先初始化了一个定时器timer,设置了timer的proc是当前进程,到期时间expires是参数time;然后把当前进程的状态设置为等待状态,且等待原因是等某个定时器到期;再调用schedule完成进程调度与切换,这时当前进程已经不占用CPU执行了。当定时器到期后,run_timer_list会删除timer且唤醒timer对应的当前进程,从而使得当前进程可以继续执行。 -------------------------------------------------------------------------------- /zh/chapter-4/wait_quence_implement.md: -------------------------------------------------------------------------------- 1 | # 等待队列设计与实现 2 | 3 | 为了支持用户进程完成特定事件的等待和唤醒操作,ucore设计了等待队列,从而使得用户进程可以方便地实现由于某事件没有完成而睡眠,并且在事件完成后被唤醒的整个操作过程。 4 | 5 | 其基本设计思想是:当一个进程由于某个事件没有产生而需要在某个睡眠等待时,设置自身运行状态为PROC_SLEEPING,等待原因为某事件,然后将自己的进程控制块指针和等待标记组装到一个数据结构为wait_t的等待项数据中,并把这个等待项的挂载到等待队列wait_queue的链表中,再执行schedule函数完成调度切换;当某些事件发生后,另一个任务(进程)会唤醒等待队列wait_queue上的某个或者所有进程,唤醒操作就是将等待队列wait_queue中的等待项中的进程运行状态设置为可调度的状态,并且把等待项从等待队列中删除。下面是等待队列的设计与实现分析。 6 | 7 | ## 数据结构描述 8 | 9 | 等待项的定义: 10 | 11 | typedef struct { 12 | struct proc_struct *proc; 13 | uint32_t wakeup_flags; 14 | wait_queue_t *wait_queue; 15 | list_entry_t wait_link; 16 | } wait_t; 17 | 18 | 这里等待项的成员变量proc表明了等待某事件的进程控制块指针,wakeup_flags是唤醒进程的事件标志(多个标志可以有逻辑或的关系,形成复合事件标志),wait_queue是此等待项所属的等待队列,wait_link用于链接到等待队列wait_queue中。 19 | 20 | 等待队列的定义: 21 | 22 | typedef struct { 23 | list_entry_t wait_head; 24 | } wait_queue_t; 25 | 26 | 等待队列就是一个双向链表的头指针。 27 | 28 | ## 等待队列相关操作函数 29 | 30 | ### 初始化 31 | 32 | 如果要使用等待队列,首先需要声明并初始化等待队列。以proj11为例,在kern/mm/swap.c中有一个等待队列的变量声明和在swap_init函数中执行的对应初始化: 33 | 34 | static wait_queue_t kswapd_done; 35 | … 36 | wait_queue_init(&kswapd_done); 37 | 38 | ### 执行等待 39 | 40 | 如果某进程需要等待某事件,则需要设置自己的运行状态为PROC_SLEEPING,构建并初始化一个等待项,再挂入到某个等待队列中。以proj11为例,某进程申请内存资源无法满足,需要等待kswapd内核线程给系统更多的内核资源,于是在try_free_pages函数中执行了如下操作: 41 | 42 | wait_t __wait, *wait = &__wait; 43 | wait_init(wait, current); 44 | current->state = PROC_SLEEPING; 45 | current->wait_state = WT_KSWAPD; 46 | wait_queue_add(&kswapd_done, wait); 47 | 48 | 这里可以看到,首先声明了一个等待项\__wait,然后调用wait_init函数对此等待项进行了初始化;并进一步把当前进程的运行状态设置为PROC_SLEEPING,睡眠原因设置为WT_KSWAPD,即等待kswapd释放出更多的空闲内存;最后把此等待项加入到等待队列kwapd_done中。 49 | 50 | ### 执行唤醒 51 | 52 | 当某个事件产生后,需要唤醒等待在等待队列中的睡眠进程。以proj11为例,当kswapd内核线程释放出更多的空闲内存后,就需要唤醒等待更多内存的进程,在kswapd内核线程的主体执行函数kswapd_main中调用了 53 | 54 | kswapd_wakup_all函数: 55 | wakeup_queue(&kswapd_done, WT_KSWAPD, 1); 56 | 57 | 这个函数就完成了唤醒功能,它会遍历kswapd_done等待队列上的所有等待项,找到一个就执行wakeup_wait函数,来进一步调用wakup_proc函数来唤醒挂在等待项上的睡眠进程。 58 | 59 | 上面是使用等待队列的基本流程。为了能够更好地完善整个基于等待队列的等待唤醒机制,在wait.[ch]中提供了一系列函数: 60 | 61 | * void wait_init:初始化等待项 62 | * void wait_queue_init:初始化等待队列 63 | * void wait_queue_add:把一个等待项加入到一个等待队列中 64 | * void wait_queue_del:从一个等待队列中删除一个等待项 65 | * wait_t *wait_queue_next:查找挂在某等待队列中的等待项指向的下一个等待项 66 | * wait_t *wait_queue_prev:查找挂在某等待队列中的等待项指向的前一个等待项 67 | * wait_t *wait_queue_first:查找挂在某等待队列中的第一个等待项 68 | * wait_t *wait_queue_last:查找挂在某等待队列中的最后一个等待项 69 | * bool wait_queue_empty:判断等待队列是否为空 70 | * bool wait_in_queue:单品某等待项是否在等待队列中 71 | * void wakeup_wait:唤醒等待项中的睡眠进程,删除等待队列中的等待项(参数del确定是否删除) 72 | * void wakeup_first:唤醒等待队列中第一个等待项中的睡眠进程,删除等待队列中的这个等待项(参数del确定是否删除) 73 | * void wakeup_queue:唤醒等待队列中所有等待项中的睡眠进程,删除等待队列中的对应等待项(参数del确定是否删除) -------------------------------------------------------------------------------- /zh/chapter-4/what_is_thread.md: -------------------------------------------------------------------------------- 1 | # 【原理】线程的属性与特征分析 2 | 3 | 线程概念的提出是计算机系统的技术发展和操作系统对进程管理优化过程的自然产物。随着计算机处理能力的提高,内存容量的加大,在一个计算机系统中会存在大量的进程,为了提高整个系统的执行效率,需要进程间能够进行简洁高效的数据共享和进程切换,进程创建和进程退出。这样就会产生一个自然的想法:能否改进进程模型,提供一个简单的数据共享机制,能否加快进程管理(特别是进程切换)的速度?再仔细看看进程管理的核心数据结构进程控制块和处理的流程,就可以发现,在某种情况下,进程的地址空间隔离不一定是一个必须的需求。假定进程间是可以相互“信任”的(即进程间是可信的),那么我们就不必需要地址空间隔离,而是让这些相互信任的进程共用(共享)一个地址空间。这样一下子就解决上述两个问题:在一个地址空间内,一个进程对某内存单元的修改(即写内存单元操作)可以让其他进程马上看见(即读内存单元操作),这是共享地址空间带来的天然好处;另外由于进程共享了地址空间,所以在创建进程或回收进程的时候,只要不是相互信任的进程组的第一个或最后一个,就没必要再创建一个地址空间或回收地址空间,节省了创建进程和退出进程的执行开销;而且对于频繁发生的进程切换操作而言,由于不需要切换页表,所以TLB中缓存的虚拟地址—物理地址映射关系(页表项的缓存)不必清除或失效,减少了进程切换的开销。 4 | 5 | 为了区别已有的进程概念,我们把这些共享资源的进程称为线程。从而把进程的概念进行了细化。原有的进程概念缩小为资源管理的最小单位,而线程是指令执行控制流的最小单位。且线程属于进程,是进程的一部分。一个进程至少需要一个线程作为它的指令执行单元,进程管理主要是资源(如内存空间等)分配、使用和回收的管理,而线程管理主要是指令执行过程和切换过程的管理。一个进程可以拥有多个线程,而这些线程共享其所属进程所拥有的资源。这样,采用多线程模型来设计应用程序,可以使得程序的执行效率也更高。 6 | 7 | 从执行线程调度时CPU所处特权级角度看,有内核级线程模型和用户级线程模型两种线程模型。内核级线程模型由操作系统在核心态进行调度,每个线程有对应的进程控制块结构。用户级线程模型有用户态的线程管理库在用户态进行调度,操作系统不能“感知”到这样的线程存在,所以也就没有对应进程控制块来描述它。相对而言,由于用户级线程模型在执行线程调度切换时不需从用户态转入核心态,开销相对较小,而内核级线程模型虽然开销相对较大,但由于操作系统直接负责管理,所以在执行的灵活性上由于用户级线程模型。比如用户级线程模型中某线程执行系统调用而可能被操作系统阻塞,这会引起同属于一个进程的其他线程都被阻塞。但内核级线程模型不会有这种情况后发生。这两种线程模型还可以结合在一起形成一种“混合”线程模型。“混合”线程模型通常都能带来更高的效率,但也带来更大的实现难度和实现代价。ucore出于“简单”的设计思路,参考Linux实现了内核级线程模型。 8 | 9 | 从线程执行时CPU所处特权级角度看,有内核线程和用户线程之分,内核线程共享操作系统内核运行过程中的所有资源,最主要的就是内核虚拟地址空间,但没有内核线程有各自独立的核心栈,且没有用户态的地址空间。用户线程共享属于同一用户进程的所有资源,最主要的就是用户虚拟地址空间,所以同享同一用户进程的进程控制块所描述的一个页表和一个内存管理数据结构。接下来我们看看ucore中如何具体实现用户线程这个概念。 10 | -------------------------------------------------------------------------------- /zh/cover/cover.md: -------------------------------------------------------------------------------- 1 | # **操作系统的基本原理与简单实现** 2 | 3 | ## *--基于ucore OS + RISC-V* 4 | 5 | 6 | - **陈渝** yuchen AT tsinghua.edu.cn 7 | - **向勇** xyong AT tsinghua.edu.cn 8 | 9 | 2018年 10 | -------------------------------------------------------------------------------- /zh/md2tex.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #set -v on 3 | if [ -d $1 ]; then 4 | echo $1 5 | cd $1 6 | for file in ./*.md 7 | do 8 | echo $file 9 | #if [ -d $file ]; then 10 | #echo $file 11 | name=`basename $file .md` 12 | echo "base name $name" 13 | pandoc -f markdown -t latex --listings -o $name.tex $name.md 14 | #fi 15 | done 16 | fi 17 | cd .. 18 | -------------------------------------------------------------------------------- /zh/preface/.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/preface/.md -------------------------------------------------------------------------------- /zh/preface/cpu.md: -------------------------------------------------------------------------------- 1 | # CPU 2 | 3 | 通用CPU一般能够在硬件上支持内存空间的隔离,使得多个程序在各自独立的内存空间中并发执行。这种硬件机制即支持用户特权级和内核特权级。应用程序运行在用户特权级,这样应用不能执行特权指令,且不能破坏操作系统内核的数据和操作系统执行过程。而操作系统内核运行在内核特权级,可以访问特权指令,并管理和控制应用程序,硬件外设等。所以对于操作系统而言,需要CPU硬件至少支持用户特权级和内核特权级(控制隔离),以及内存空间隔离(数据隔离)。 4 | 5 | ## RISC-V的CPU运行模式 6 | 7 | 80386处理器有四种运行模式:Machine模式、Hypervisor模式、Supervisor模式和user模式。所以这里对实模式、保护模式做一个简要介绍。 8 | 9 | 实模式:这是个人计算机早期的8086处理器采用的一种简单运行模式,当时微软的MS-DOS操作系统主要就是运行在8086的实模式下。80386加电启动后处于实模式运行状态,在这种状态下软件可访问的物理内存空间不能超过1MB,且无法发挥Intel 80386以上级别的32位CPU的4GB内存管理能力。实模式将整个物理内存看成分段的区域,程序代码和数据位于不同区域,操作系统和用户程序并没有区别对待,而且每一个指针都是指向实际的物理地址。这样用户程序的一个指针如果指向了操作系统区域或其他用户程序区域,并修改了内容,那么其后果就很可能是灾难性的。 10 | 11 | > 对于ucore其实没有必要涉及,这主要是Intel x86的向下兼容需求导致其一直存在。其他一些CPU,比如ARM、MIPS等就没有实模式,而是只有类似保护模式这样的CPU模式。 12 | 13 | 保护模式:保护模式的一个主要目标是确保应用程序无法对操作系统进行破坏。实际上,80386就是通过在实模式下初始化控制寄存器(如GDTR,LDTR,IDTR与TR等管理寄存器)以及页表,然后再通过设置CR0寄存器使其中的保护模式使能位置位,从而进入到80386的保护模式。当80386工作在保护模式下的时候,其所有的32根地址线都可供寻址,物理寻址空间高达4GB。在保护模式下,支持内存分页机制,提供了对虚拟内存的良好支持。保护模式下80386支持多任务,还支持优先级机制,不同的程序可以运行在不同的特权级上。特权级一共分0~3四个级别,操作系统运行在最高的特权级0上,应用程序则运行在比较低的级别上;配合良好的检查机制后,既可以在任务间实现数据的安全共享也可以很好地隔离各个任务。 14 | 15 | > 这一段中很多术语没有解释,在后续的章节中会逐一展开阐述。 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /zh/preface/figures/io_arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/preface/figures/io_arch.png -------------------------------------------------------------------------------- /zh/preface/figures/mem_arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/preface/figures/mem_arch.png -------------------------------------------------------------------------------- /zh/preface/figures/os_interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/preface/figures/os_interface.png -------------------------------------------------------------------------------- /zh/preface/figures/oslab-chapt1-106125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/preface/figures/oslab-chapt1-106125.png -------------------------------------------------------------------------------- /zh/preface/figures/oslab-chapt1-106501.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/preface/figures/oslab-chapt1-106501.png -------------------------------------------------------------------------------- /zh/preface/figures/oslab-chapt1-106722.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/preface/figures/oslab-chapt1-106722.png -------------------------------------------------------------------------------- /zh/preface/figures/oslab-chapt1-106926.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/preface/figures/oslab-chapt1-106926.png -------------------------------------------------------------------------------- /zh/preface/figures/pc_arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/preface/figures/pc_arch.png -------------------------------------------------------------------------------- /zh/preface/figures/software-stacks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/preface/figures/software-stacks.png -------------------------------------------------------------------------------- /zh/preface/figures/ucore_arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/preface/figures/ucore_arch.png -------------------------------------------------------------------------------- /zh/preface/hardware.md: -------------------------------------------------------------------------------- 1 | # 了解计算机硬件架构 2 | 3 | 操作系统作为软件,首先要能在计算机硬件上运行,并完成其第一要务:对硬件进行管理和控制。要想深入理解操作系统,就需要了解支撑操作系统运行的硬件环境,即了解处理器体系结构和机器指令集,来探索CPU对操作系统的影响,以及操作系统如何通过各种操作来管理硬件的。当前的硬件和CPU有很多种,在细节上有很大的不同,如果一开始就陷入硬件细节,不利于同学对OS和计算机硬件之间的关系建立一个比较全面和概览性的理解。所以,我们将先简单介绍一个抽象的简化计算机系统,然后在逐步进入到某一具体CPU,即RISC-V,的硬件细节中。 4 | 5 | -------------------------------------------------------------------------------- /zh/preface/io.md: -------------------------------------------------------------------------------- 1 | ## 外设 2 | 3 | 计算机系统中的硬件设备(外设)千差万别,一般连接在计算机系统中I/O总线上,通过I/O控制器与CPU进行交互。I/O控制器在物理上包含三个层次:I/O地址空间、I/O接口和设备控制器。每个连接到I/O总线上的设备都有自己的I/O地址空间(即I/O端口),这也是CPU可以直接访问的地址。CPU一般支持I/O地址空间访问,即通过特定的I/O访问指令访问;也支持基于内存的I/O地址空间,即通过一般的访存指令访问。这些I/O访问请求通过I/O总线传递给I/O接口。 4 | 5 | I/O接口是处于一组I/O端口和对应的设备控制器之间的一种硬件电路。它将I/O访问请求中的特定值转换成设备所需要的命令和数据;并且检测设备的状态变化,及时将各种状态信息写回到特定I/O地址空间,供操作系统通过I/O访问指令来访问。I/O接口包括键盘接口、图形接口、磁盘接口、总线鼠标、网络接口、括并口、串口、通用串行总线、PCMCIA接口和SCSI接口等。 6 | 7 | 设备控制器并不是所有I/O设备所必须的,只有少数复杂的设备才需要。它负责解释从I/O接口接收到的高级命令,并将其以适当的方式发送到I/O设备;并且对I/O设备发送的消息进行解释并修改I/O端口的状态寄存器。典型的设备控制器就是磁盘控制器,它将CPU发送过来的读写数据指令转换成底层的磁盘操作。 8 | 9 | 操作系统对硬件设备的控制方式主要与三种:程序循环检测方式\(Programmed I/O,简称PIO\)、中断驱动方式\(Interrupt-driven I/O\)、直接内存访问方式\(DMA, Direct Memory Access\)。 10 | 11 | -------------------------------------------------------------------------------- /zh/preface/memory.md: -------------------------------------------------------------------------------- 1 | # 内存 2 | 3 | 内存是用于存放代码和数据地址的硬件,访问速度快,空间大。为高效定位代码和数据的位置,需要建立内存地址,即访问内存空间的索引。一般而言,内存地址有两个:一个是CPU通过总线访问物理内存用到的物理地址,一个是我们编写的应用程序所用到的逻辑地址(也有人称为虚拟地址)。比如如下C代码片段: 4 | 5 | ``` 6 | int boo=1; 7 | int *foo=&a; 8 | ``` 9 | 10 | 这里的boo是一个整型变量,foo变量是一个指向boo地址的整型指针变量,foo中储存的内容就是boo的逻辑地址。 11 | 12 | 对于一般的32位CPU而言,以寻址的物理内存地址空间为2\^32=4G字节,支持以页(页大小一般为4KB)为单位对内物理内存空间进行重新编排内存地址,形成虚拟内存地址,而编排虚拟内存地址的策略由操作系统完成。这样操作系统就可以指定不同的物理内存空间给应用程序,而应用程序“看到”的是操作系统在CPU的支持下虚拟化后的地址空间。最终,让操作系统可以更灵活地安排应用程序所占用的内存空间,也简化了应用程序对内存空间的管理。 13 | 14 | ## x86的内存管理 15 | 16 | 80386是32位的处理器,即可以寻址的物理内存地址空间为2\^32=4G字节。为更好理解面向80386处理器的ucore操作系统,需要用到三个地址空间的概念:物理地址、线性地址和逻辑地址。物理内存地址空间是处理器提交到总线上用于访问计算机系统中的内存和外设的最终地址。一个计算机系统中只有一个物理地址空间。线性地址空间是80386处理器通过段(Segment)机制控制下的形成的地址空间。在操作系统的管理下,每个运行的应用程序有相对独立的一个或多个内存空间段,每个段有各自的起始地址和长度属性,大小不固定,这样可让多个运行的应用程序之间相互隔离,实现对地址空间的保护。 17 | 18 | 在操作系统完成对80386处理器段机制的初始化和配置(主要是需要操作系统通过特定的指令和操作建立全局描述符表,完成虚拟地址与线性地址的映射关系)后,80386处理器的段管理功能单元负责把虚拟地址转换成线性地址,在没有下面介绍的页机制启动的情况下,这个线性地址就是物理地址。 19 | 20 | 相对而言,段机制对大量应用程序分散地使用大内存的支持能力较弱。所以Intel公司又加入了页机制,每个页的大小是固定的(一般为4KB),也可完成对内存单元的安全保护,隔离,且可有效支持大量应用程序分散地使用大内存的情况。 21 | 22 | 在操作系统完成对80386处理器页机制的初始化和配置(主要是需要操作系统通过特定的指令和操作建立页表,完成虚拟地址与线性地址的映射关系)后,应用程序看到的逻辑地址先被处理器中的段管理功能单元转换为线性地址,然后再通过80386处理器中的页管理功能单元把线性地址转换成物理地址。 23 | 24 | > 页机制和段机制有一定程度的功能重复,但Intel公司为了向下兼容等目标,使得这两者一直共存。 25 | 26 | 上述三种地址的关系如下: 27 | 28 | * 分段机制启动、分页机制未启动:逻辑地址--->_**段机制处理**_--->线性地址=物理地址 29 | 30 | * 分段机制和分页机制都启动:逻辑地址--->_**段机制处理**_--->线性地址--->_**页机制处理**_--->物理地址 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /zh/preface/memory/.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chyyuu/simple_os_book/a735d57f7353dc8895687759527019ebe7f94dac/zh/preface/memory/.md -------------------------------------------------------------------------------- /zh/preface/osabstract.md: -------------------------------------------------------------------------------- 1 | # 操作系统抽象 2 | 3 | 接下来读者可站在操作系统实现的角度来看操作系统。操作系统为了能够更好地管理计算机系统并对应用程序提供便捷的服务,在操作系统的发展过程中,计算机科学家提出了如下四个个抽象概念,奠定了操作系统内核设计与实现的基础。操作系统原理中的其他基本概念基本上都基于上述这四个操作系统抽象。 4 | 5 | ## **中断(Interrupt)** 6 | 7 | 简单地说,中断是处理器在执行过程中的突变,用来响应处理器状态中的特殊变化。比如当应用程序正在执行时,产生了时钟外设中断,导致操作系统打断当前应用程序的执行,转而去处理时钟外设中断,处理完毕后,再回到应用程序被打断的地方继续执行。在操作系统中,有三类中断:外设中断(Device Interrupt)、陷阱中断(Trap Interrupt)和故障中断(Fault Interrupt,也称为exception,异常)。外设中断由外部设备引起的外部I/O事件如时钟中断、控制台中断等。外设中断是异步产生的,与处理器的执行无关。故障中断是在处理器执行指令期间检测到不正常的或非法的内部事件(如除零错、地址访问越界)。陷阱中断是在程序中使用请求操作系统服务的系统调用而引发的有意事件。在后面的叙述中,如果没有特别指出,我们将用简称中断、陷阱、故障来区分这三种特殊的中断事件,在不需要区分的地方,统一用中断表示。 8 | 9 | ## **进程(Process)** 10 | 11 | 简单地说,进程是一个正在运行的程序。在计算机系统中,我们可以“同时”运行多个程序,这个“同时”,其实是操作系统给用户造成的一个“幻觉”。大家知道,处理器是计算机系统中的硬件资源。为了提高处理器的利用率,操作系统采用了多道程序技术。如果一个程序因某个事件而不能运行下去时,就把处理器占用权转交给另一个可运行程序。为了刻画多道程序的并发执行的过程,就要引入进程的概念。从操作系统原理上看,一个进程是一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程。操作系统中的进程管理需要协调多道程序之间的关系,解决对处理器分配调度策略、分配实施和回收等问题,从而使得处理器资源得到最充分的利用。 12 | 13 | ## **虚存(Virtual Memory)** 14 | 15 | 简单地说,虚存就是操作系统通过处理器中的MMU硬件的支持而给应用程序和用户提供一个大的(超过计算机中的内存条容量)、一致的(连续的地址空间)、私有的(其他应用程序无法破坏)的存储空间。这需要操作系统将内存和硬盘结合起来管理,为用户提供一个容量比实际内存大得多的虚拟存储器,并且需要操作系统为应用程序分配内存空间,使用户存放在内存中的程序和数据彼此隔离、互不侵扰。操作系统中的虚存管理与处理器的MMU密切相关。 16 | 17 | ## **文件(File)** 18 | 19 | 简单地说,文件就是存放在持久存储介质(比如硬盘、光盘、U盘等)上,方便应用程序和用户读写的数据。当处理器需要访问文件中的数据时,可通过操作系统把它们装入内存。放在硬盘上的程序也是一种文件。文件管理的任务是有效地支持文件的存储、检索和修改等操作。 20 | -------------------------------------------------------------------------------- /zh/preface/osconcept.md: -------------------------------------------------------------------------------- 1 | ### 操作系统的定义与目标 2 | 3 | ### 操作系统的定义 4 | 5 | 有了对硬件的进一步了解,我们就可以给操作系统下一个更准确一些的定义。操作系统是计算机系统机构中的一个系统软件,它的职能主要有两个:对下面(也就是计算机硬件),有效地组织和管理计算机系统中的硬件资源(包括处理器、内存、硬盘、显示器、键盘、鼠标等各种外设);对上面(应用程序或用户),提供简洁的服务功能接口,屏蔽硬件管理带来的差异性和复杂性,使得应用程序和用户能够灵活、方便、有效地使用计算机。为了完成这两个职能,操作系统需要起到资源管理器的作用,能在其内部实现中安全,合理地组织,分配,使用与处理计算机中的软硬件资源,使整个计算机系统能高效可靠地运行。 6 | 7 | ### 操作系统的目标 8 | 9 | 根据前面的介绍,我们可以看出操作系统有如下一些目标: 10 | 11 | * 建立抽象,让上层软件和用户更方便使用; 12 | * 管理软硬件资源,确保计算机系统安全可靠、高性能; 13 | * 其他需求:节能、易用、可移植、实时等等 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /zh/preface/osfeature.md: -------------------------------------------------------------------------------- 1 | # 操作系统特征 2 | 3 | 基于操作系统的四个抽象,我们可以看出,从总体上看,操作系统具有五个方面的特征:虚拟性(Virtualization)、并发性(concurrency)、异步性、共享性和持久性(persistency)。在虚拟性方面,可以从操作系统对内存,CPU的抽象和处理上有更好的理解;对于并发性和共享性方面,可以从操作系统支持多个应用程序“同时”运行的情况来理解;对于异步性,可以从操作系统调度,中断处理对应用程序执行造成的影响等几个放马来理解;对于持久性方面,可以从操作系统中的文件系统支持把数据方便地从磁盘等存储介质上存入和取出来理解。 4 | 5 | ## 虚拟性 6 | 7 | ### 内存虚拟化 8 | 9 | 首先来看看内存的虚拟化。程序员在写应用程序的时候,不用考虑其程序的起始内存地址要放到计算机内存的具体某个位置,而是用字符串符号定义了各种变量和函数,直接在代码中便捷地使用这些符号就行了。这是由于操作系统建立了一个**地址固定**,**空间巨大**的虚拟内存给应用程序来运行,这是**空间虚拟化**。这里的每个符号在运行时是要对应到具体的内存地址的。这些内存地址的具体数值是什么?程序员不用关心。为什么?因为编译器会自动帮我们吧这些符号翻译成地址,形成可执行程序。程序使用的内存是否占得太大了?在一般情况下,程序员也不用关心。 10 | 11 | > 还记得虚拟地址(逻辑地址)的描述吗? 12 | > 但编译器\(compiler,比如gcc\)和链接器(linker,比如ld)也不知道程序每个符号对应的地址应该放在未来程序运行时的哪个物理内存地址中。所以,编译器的一个简单处理办法就是,设定一个固定地址(比如 0x10000)作为起始地址,开始存放代码,代码之后是数据,所有变量和函数的符号都在这个起始地址之后的某个固定偏移位置。假定程序每次运行都是位于一个不会变化的起始地址。 13 | > 这里的变量指的是全局变量,其地址在编译链接后会确定不变。但局部变量是放在堆栈中的,会随着堆栈大小的动态变化而变化。 14 | > 这里编译器产生的地址就是虚拟地址。 15 | > 这里,编译器和链接器图省事,找了一个适合它们的解决办法。当程序要运行的时候,这个符号到机器物理内存的映射必须要解决了,这自然就推到了操作系统身上。操作系统会把编译器和链接器生成的执行代码和数据放到物理内存中的空闲区域中,并建立虚拟地址到物理地址的映射关系。由于物理内存中的空闲区域是动态变化的,这也导致虚拟地址到物理地址的映射关系是动态变化的,需要操作系统来维护好可变的映射关系,确保编译器“固定起始地址”的假设成立。只有操作系统维护好了这个映射关系,才能让程序员只需写一些易于人理解的字符串符号来代表一个内存空间地址,且编译器只需确定一个固定地址作为程序的起始地址就可以生成一个不用考虑将来这个程序要在哪里运行的问题,从而实现了**空间虚拟化**。 16 | 17 | 应用程序在运行时不用考虑当前物理内存是否够用。如果应用程序需要一定空间的内存,但由于在某些情况下,物理内存的空闲空间可能不多了,这时操作系统通过把物理内存中最近没使用的空间(不是空闲的,只是最近用得少)换出(就是“挪地”)到硬盘上暂时缓存起来,这样空闲空间就大了,就可以满足应用程序的运行时内存需求了,从而实现了**空间大小虚拟化**。 18 | 19 | ### CPU虚拟化 20 | 21 | 再来看CPU虚拟化。不同的应用程序可以在内存中并发运行,相同的应用程序也可有多个拷贝在内存中并发运行。而每个程序都“认为”自己完全独占了CPU在运行,这是”时间虚拟化“。这其实也是操作系统给了运行的应用程序一个虚拟幻象。其实是操作系统把时间分成小段,每个应用程序占用其中一小段时间片运行,用完这一时间片后,操作系统会切换到另外一个应用程序,让它运行。由于时间片很短,操作系统的切换开销也很小,人眼基本上是看不出的,反而感觉到多个程序各自在独立”并行“执行,从而实现了**时间虚拟化**。 22 | 23 | > 并行(Parallel)是指两个或者多个事件在同一时刻发生;而并发(Concurrent)是指两个或多个事件在同一时间间隔内发生。 24 | > 对于单CPU的计算机而言,各个”同时“运行的程序其实是串行分时复用一个CPU,任一个时刻点上只有一个程序在CPU上运行。 25 | > 这些虚拟性的特征给应用程序的开发和执行提供了非常方便的环境,但也给操作系统的设计与实现提出了很多挑战。 26 | 27 | ## 并发性 28 | 29 | 操作系统为了能够让CPU充分地忙起来并充分利用各种资源,就需要给很多任务给它去完成。这些任务是分时完成的,有操作系统来完成各个应用在运行时的任务切换。并发性虽然能有效改善系统资源的利用率,但并发性也带来了对共享资源的争夺问题,即同步互斥问题;执行时间的不确定性问题,即并发程序在执行中是走走停停,断续推进的。并发性对操作系统的设计也带来了很多挑战,一不小心就会出现程序执行结果不确定,程序死锁等很难调试和重现的问题。 30 | 31 | ## 异步性 32 | 33 | 在这里,异步是指由于操作系统的调度和中断等,会不时地暂停或打断当前正在运行的程序,使得程序的整个运行过程走走停停。在应用程序运行的表现上,特别它的执行完成时间是不可预测的。但需要注意,只要应用程序的输入是一致的,那么它的输出结果应该是符合预期的。 34 | 35 | ## 共享性 36 | 37 | 共享是指多个应用并发运行时,宏观上体现出它们可同时访问同一个资源,即这个资源可被共享。但其实在微观上,操作系统在硬件等的支持下要确保应用程序互斥或交替访问这个共享的资源。比如两个应用同时写访问同一个内存单元,从宏观的应用层面上看,二者都能正确地读出同一个内存单元的内容。而在微观上,操作系统会调度应用程序的先后执行顺序,在数据总线上任何一个时刻,只有一个应用去访问存储单元。 38 | 39 | ## 持久性 40 | 41 | 操作系统提供了文件系统来从可持久保存的存储介质(硬盘,SSD等,以后以硬盘来代表)中取数据和代码到内存中,并可以把内存中的数据写回到硬盘上。硬盘在这里是外设,具有持久性,以文件系统的形式呈现给应用程序。 42 | 43 | > 文件系统也可看成是操作系统对硬盘的虚拟化 44 | > 这种持久性的特征进一步带来了共享属性,即在文件系统中的文件可以被多个运行的程序所访问,从而给应用程序之间实现数据共享提供了方便。即使掉电,磁盘上的数据还不会丢失,可以在下一次机器加电后提供给运行的程序使用。持久性对操作系统的执行效率提出了挑战,如何让数据在高速的内存和慢速的硬盘间高效流动是需要操作系统考虑的问题。 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /zh/preface/oshistory.md: -------------------------------------------------------------------------------- 1 | 在大众的眼中,操作系统就是他们的手机/终端上的软件系统,包括各种应用程序集合,但在历史上,操作系统也是从无到有地逐步发展起来的。操作系统主要完成对硬件控制和对应用程序的服务所必需的功能,操作系统的历史与计算机发展的历史密不可分。操作系统的内涵和功能随着历史的发展也在一直变化,改进中,在今天,没有图形界面和各种文件浏览器已经不能称为一个操作系统了。 2 | 3 | ## 三叶虫时代 4 | 5 | 计算机在最开始出现的时候是没有操作系统的。启动,扳开关,装卡片/纸带等比较辛苦的工作都是计算机操作员(Operator)或者用户自己完成。操作员/用户带着记录有程序和数据的卡片\(punch card\)或打孔纸带去操作机器。装好卡片/纸带后,启动卡片/纸带阅读器,让计算机把程序和数据读入计算机机的内存中后,计算机就开始工作,并把结果也输出到卡片/纸带或显示屏上,最后程序停止。 6 | 7 | 由于人的操作效率太低,计算机的机时宝贵,所以就引入监控程序(Monitor)辅助完成输入,输出,加载,运行程序等工作,这是现代操作系统的起源。一般情况下,计算机每次只能执行一个任务,CPU大部分时间都在等待人的缓慢操作。 8 | 9 | ## 恐龙时代 10 | 11 | 早期的操作系统非常多样化,专用化,生产商生产出针对各自硬件的专用操作系统,大部分用汇编语言编写,这导致操作系统的进化比较缓慢,但进化再持续。在1964年,IBM公司开发了面向System/360系列机器的统一可兼容的操作系统——OS/360。OS/360是一种批处理操作系统。为了能充分地利用计算机系统,应尽量使该系统连续运行,减少空闲时间,所以批处理操作系统把一批作业(古老的术语,可理解为现在的程序)以脱机方式输入到磁带上,并使这批作业能一个接一个地连续处理:1)将磁带上的一个作业装入内存;2)并把运行控制权交给该作业;3)当该作业处理完成后,把控制权交还给操作系统;4)重复1-3的步骤。 12 | 13 | 批处理操作系统分为单道批处理系统和多道批处理系统。单道批处理操作系统只能管理内存中的一个(道)作业,无法充分利用计算机系统中的所有资源,致使系统整体性能较差。多道批处理操作系统能管理内存中的多个(道)作业,可比较充分地利用计算机系统中的所有资源,提升系统整体性能。二者的共同特点是人机交互性差,这对修改和调试程序很不方便。 14 | 15 | ## 爬行动物时代 16 | 17 | 20世纪60年代末,提高人机交互方式的分时操作系统越来越展露头角。分时是指多个用户和多个程序以很小的时间间隔来共享使用同一台计算机上的CPU和其他硬件/软件资源。1964年由贝尔实验室、麻省理工学院及美国通用电气公司所共同参与研发目标远大的MULTICS\(MULTiplexed Information and Computing System\)操作系统,MULTICS是一套安装在大型主机上多人多任务的操作系统。 MULTICS以兼容分时系统(CTSS)做基础,建置在美国通用电力公司的大型机GE-645,目标是连接1000部终端机,支持300的用户同时上线。因MULTICS项目的工作进度过于缓慢,1969年AT&T的 Bell 实验室从MULTICS 研发中撤出。但贝尔实验室的两位软件工程师 Thompson 与 Ritchie借鉴了一些重要的Multics理念,以C语言为基础,发展出UNIX操作操作系统。UNIX操作系统的早期版本是完全免费的,可以轻易获得并随意修改,所以它得到了广泛的接受。后来,它成为开发小型机操作系统的起点。由于早期的广泛应用,它已经成为的分时操作系统的典范。 18 | 19 | ## 哺乳动物时代 20 | 21 | 20世纪70年代,微型处理器的发展使计算机的应用普及至中小企及个人爱好者,推动了个人计算机\(Personal Computer\)的发展,也进一步推动了面向个人使用的操作系统的出现。其代表是由微软公司中在20世纪80年代为个人计算机开发的DOS/Windows操作系统,其特点是简单易用,特别是基于Windwos操作系统的GUI界面,极大地简化了一般用户使用计算机的难度,使得计算机得到了快速的普及。这里需要注意的是,第一个带GUI界面的个人计算机原型起源于伟大却又让人扼腕叹息的施乐帕洛阿图研究中心PARC(Palo Alto Research Center),PARC研发出的带有图标、弹出式菜单和重叠窗口的GUI(Graphical User Interface),可利用鼠标的点击动作来进行操控,这是当今我们所使用的GUI系统的基础。 22 | 23 | ## 智人时代 24 | 25 | 21世纪以来,Internt和移动互联网的迅猛发展,使得在服务器领域和个人终端的应用与需求大增。iOS和Android操作系统是21世纪个人终端操作系统的代表,Linux在巨型机到数据中心服务器操作系统中占据了统治地位。以Android系统为例,Android一词英文本义指“机器人”,它是由Google公司于2007年11月推出的基于Linux Kernel的开源手机操作系统,目前在移动终端中占有最大的份额。Android操作系统是一个包括Linux操作系统内核、基于Java的中间件、用户界面和关键应用软件的移动设备软件栈集合。这里介绍一下广泛用在服务器领域和个人终端中的操作系统内核--Linux操作系统内核。1991年8 月,芬兰学生 Linus Torvalds\(林纳斯·托瓦兹\)在 comp.os.minix 新闻组贴上了以下这段话:  26 | 27 | ``` 28 |   "你好,所有使用 minix 的人 -我正在为 386 ( 486 ) AT 做一个免费的操作系统 ( 只是为了爱好 ),不会像 GNU 那样很大很专业。″ 29 | ``` 30 | 31 | 而他所说的"爱好″就变成我们今天知道的 Linux操作系统内核。 Linus通过Internet首次发表 Linux kernel的源代码,并且选用GPL版权协议来发行。GPL版权协议允许任何人以任何形式发布 Linux 的源代码,在Internet的日渐盛行以及 Linux 开放自由的GPL版权之下,吸引了无数计算机Hacker和公司投入开发、改善 Linux kernel,使得 Linux kernel的功能日见强大。  32 | 33 | ## 神人时代 34 | 35 | 当前,大数据、人工智能、机器学习、高速网络、AR/VR对操作系统等系统软件带来了新的挑战。如何有效支持和利用这些技术是未来操作系统的方向。 36 | 37 | -------------------------------------------------------------------------------- /zh/preface/osinterface.md: -------------------------------------------------------------------------------- 1 | # 操作系统的接口 2 | 3 | 首先,读者可站在使用操作系统的角度来看操作系统。操作系统内核是一个需要提供各种服务的软件,其服务对象是应用程序,而用户(这里可以理解为一般使用计算机的人)是通过应用程序的服务间接获得操作系统的服务的),所以操作系统内核藏在一般用户看不到的地方。但应用程序需要访问操作系统,获得操作系统的服务,这就需要通过操作系统的接口才能完成。如果把操作系统看成是一个函数库,那么其接口就是函数名称和它的参数。但操作系统不是简单的一个函数库,它的接口需要考虑安全因素,使得应用软件不能直接读写操作系统内部函数的地址地址空间,为此,操作系统设计了一个安全可靠的接口,我们称为系统调用接口(System Call Interface),应用程序可以通过系统调用接口请求获得操作系统的服务,但不能直接调用操作系统的函数和全局变量;操作系统提供完服务后,返回应用程序继续执行。 4 | 5 | 对于实际操作系统而言,具有大量的服务接口,比如Linux有上百个系统调用接口。为了简单起见,以ucore OS为例,可以看到它为应用程序提供了如下一些接口: 6 | 7 | * 进程管理:复制创建--fork、退出--exit、执行--exec、... 8 | * 同步互斥的并发控制:信号量--semaphore、管程--monitor、条件变量--condition variable 、... 9 | * 进程间通信:管道--pipe、信号--signal、事件--event、邮箱--mailbox、共享内存--shared mem、... 10 | * 文件I/O操作:读--read、写--write、打开--open、关闭--close、... 11 | * 外设I/O操作:外设包括键盘、显示器、串口、磁盘、时钟、...,但接口是直接采用了文件I/O操作的系统调用接口 12 | 13 | > 这在某种程度上说明了文件是外设的一种抽象。在UNIX中(ucore是模仿UNIX),大部分外设都可以以文件的形式来访问 14 | 15 | 有了这些接口,简单的应用程序就不用考虑底层硬件细节,可以在操作系统的服务支持和管理下简洁地完成其应用功能了。 16 | 17 | 18 | 19 | ![](/zh/preface/figures/os_interface.png) 20 | 21 | -------------------------------------------------------------------------------- /zh/preface/pc.md: -------------------------------------------------------------------------------- 1 | # 一般计算机硬件架构 2 | 3 | 这里介绍一下运行操作系统的基本计算机硬件架构。一台计算机可抽象一台以图灵机(Turing Machine)为理想模型,以冯诺依曼架构( Von Neumann Architecture)为实现模型的电子设备,包括CPU、memory和 I/O 设备。CPU\(中央处理器,也称处理器\) 执行操作系统中的指令,完成相关计算和读写内存,物理内存保存了操作系统中的指令和需要处理的数据,外部设备用于实现操作系统的输入(键盘、硬盘),输出(显示器、并口、串口),计时(时钟)永久存储(硬盘)。操作系统除了能在计算机上运行外,还要管好计算机。下面将简要介绍计算机硬件以及操作系统大致要完成的事情。 4 | 5 | ![计算机抽象图](figures/pc_arch.png) 6 | 7 | ## CPU 8 | 9 | CPU是计算机系统的核心,目前存在各种8位,16位,32位,64位的CPU,应用在从嵌入式系统到巨型机等不同的场合中。CPU从一加电开始,从某设定好的内存地址开始,取指令,执行指令,并周而复始地运行。取指令的过程即从某寄存器(比如,程序计数器)中获取一个内存地址,从这个内存地址中读入指令,执行机器指令,不断重复,CPU运行期间会有分支和调用指令来修改程序计数器,实现地址跳转,否则程序计数器就自动加1,让CPU从下一个内存地址单元取指令,并继续执行。 10 | 11 | 由于CPU执行速度很快(x86 CPU可达到2GHZ以上的时钟频率,RISC-V CPU可达到1.5 GHZ的时钟频率),如果当前可以运行的程序太少,则会出现CPU无事可做的情况,导致计算机系统效率太低。这时就需要操作系统来帮忙了,我们需要操作系统除了能管理硬件外,还能管理应用程序,让它们能够按一定的顺序和优先级来依次使用CPU,充分发挥CPU的效能。操作系统管一个程序比较容易,但如果管理多个程序的运行,就需要考虑如何分配CPU资源的问题,如何避免程序执行期间发生“冲突”的问题等,这是操作系统需要完成的重要功能之一。 12 | 13 | ## memory 14 | 15 | ![内存层次图](figures/mem_arch.png) 16 | 17 | 计算机中有多种多层次的存放数据和指令代码的硬件存储单元,比如在CPU内的寄存器(register)、高速缓存\(cache\)、内存(memory)、硬盘、磁带等。寄存器位于CPU内部,其访问速度最快但成本昂贵,在对于传统的CISC(复杂指令集计算机,如Intel 80386处理器)中一般只有几个到十个左右的通用寄存器,而对于RISC(精简指令集计算机,如RISC-V),则可能有几十个以上通用寄存器;高速缓存(cache) 一般也在CPU内部,cache是内存和寄存器在速度和大小上的折衷,比寄存器慢2~10倍,容量也有限,量级大约几百KB到几十MB不等;再接下来就是内存了,内存位于CPU外,比寄存器慢10倍以上,但容量大,目前一般以几百兆B到几百GB不等;硬盘容量更大,但一般比寄存器要慢1000倍以上,不过掉电后其存储的数据不会丢失。 18 | 19 | 由于寄存器、cache、内存、硬盘在读写速度和容量上的巨大差异,所以需要操作系统来协调数据的访问,尽量主动协助应用软件,把最近访问的数据放到寄存器或cache中(实际上操作系统不能直接控制cache的读写),把经常访问的数据放在内存中,把不常用的数据放到硬盘上,这样可以达到让多个运行的应用程序“感觉”到它可用使用很大的空间,也可有很快的访问速度。如何让在运行中的每个程序都能够得到“足够大”的内存空间,且程序间相互不能破坏对方的内存“领地”,且建立他们之间的“数据共享”通道,这是操作系统需要完成的重要功能之一。 20 | 21 | ## I/O 22 | 23 | ![IO设备图](figures/io_arch.png) 24 | 25 | CPU处理的数据需要有来源和输出,这里的来源和输出就是各种外设,如大家日常使用计算机用到的键盘、鼠标、显示器、声卡、GPU、U盘、硬盘、SSD存储、打印机、网卡、摄像头等。上图中给出了PC计算机的一些外设硬件。应用程序如果直接访问外设,会有代码实现复杂,可移植性差,无法有效并发等问题,所以操作系统给应用程序提供了简单的访问接口,让应用程序不需要了解硬件细节。具体访问外设的苦活累活都交给操作系统来完成了,这就是操作系统中外设驱动哦程序的主要功能。 26 | 27 | 如果操作系统要通过CPU对数据进行加工,首先需要有输入,在处理完后还要进行输出,否则没东西要处理或者执行完了无法把结果反馈给用户。操作系统要处理的数据需要从外设(比如键盘、串口、硬盘、网卡)中获得,这就是一种读外设数据的操作;且在处理完毕后要传给外设(比如显示器、硬盘、打印机、网卡)进一步处理,这其实就是一种写外设数据的操作。 28 | 29 | 一般而言,IO外设把它的访问接口映射为一块内存区域,操作系统通过来用通常的内存读写指令来管理设备;或者CPU提供了特定的IO操作指令,操作系统通过这些特定的指令来完成对IO外设的访问。并且操作系统可以通过轮循、中断、DMA等访问方式来高效地管理外设。 30 | 31 | 比如,在RISC-V中通过对特定地址的内存(代表IO外设的接口)进行读或写访问,就可以实现对IO外设的访问了。在Intel x86中有两条特殊的 `in`和`out` 指令来在完成CPU对外设地址空间的访问,实现对外设的管理控制。本书不会涉及很多复杂具体硬件,而只涉及到操作系统用到的一些最基本的外设硬件(时钟,串口,硬盘)细节。 32 | 33 | -------------------------------------------------------------------------------- /zh/preface/preface.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | ## 用一句话描述本章 4 | 5 | 站在一万米的高空看操作系统的发展和特征! 6 | 7 | ## 本章概述 8 | 9 | 本章站在一万米的高空来看操作系统和计算机原理。相对用软件而言,操作系统其实是一个相比较复杂的系统软件,直接管理计算机硬件和各种外设,以及给应用软件提供帮助。这样描述还太简单了一些,我们可对其进一步描述:操作系统是一个可以管理CPU、内存和各种外设,并管理和服务应用软件的软件。为了完成这些工作,操作系统需要知道如何与硬件打交道,如何更好地面向应用软件做好服务。 10 | 11 | 本章将讲述操作系统学习的一些基础知识,以及对用于本书的ucore教学操作系统做一个介绍。然后再简单介绍操作系统的基本概念、操作系统抽象以及操作系统的特征。最后还将简要介绍操作系统的历史和基本架构。 12 | 13 | -------------------------------------------------------------------------------- /zh/preface/preknowledge.md: -------------------------------------------------------------------------------- 1 | 本书希望通过设计实现操作系统来更好地理解操作系统原理和概念。设计实现操作系统其实就是设计实现一个可以管理CPU、内存和各种外设,并管理和服务应用软件的系统软件。为此还是需要先了解一些基本的计算机原理和编程的知识。本书的例子和描述需要读者学习过计算机原理课程、程序设计课程,掌握C语言编程(了解指针等的编程)。如需完成基于RISC-V的ucore实验,则对基于RISC-V的体系结构有一定的了解,大致了解RISC-V的汇编语言。 2 | 3 | -------------------------------------------------------------------------------- /zh/preface/smallos_ucore.md: -------------------------------------------------------------------------------- 1 | # “麻雀“OS--uCore 2 | 3 | 为了学习OS,需要了解一个上百万代码的操作系统吗?自己写一个操作系统难吗?别被现在上百万行的Linux和Windows操作系统吓倒。当年Thompson乘他老婆带着小孩度假留他一人在家时,写了UNIX;当年Linus还是一个21岁大学生时完成了Linux雏形。站在这些巨人的肩膀上,我们能否也尝试一下做“巨人”的滋味呢? 4 | 5 | MIT的Frans Kaashoek等在2006年参考PDP-11上的UNIX Version 6写了一个可在X86上跑的操作系统xv6(基于MIT License),用于学生学习操作系统。我们可以站在他们的肩膀上,基于xv6的设计,尝试着一步一步完成一个从“空空如也”到“五脏俱全”的“麻雀”操作系统—ucore,此“麻雀”包含虚存管理、进程管理、处理器调度、同步互斥、进程间通信、文件系统等主要内核功能,总的内核代码量(C+asm)不会超过5K行。充分体现了“小而全”的指导思想。 6 | 7 | ucore的运行环境可以是真实的X86计算机,不过考虑到调试和开发的方便,我们可采用X86模拟器,比如QEMU、BOCHS等,或X86虚拟运行环境,比如VirtualBox、VMware Player等。ucore的开发环境主要是GCC中的gcc、gas、ld和MAKE等工具,也可采用集成了这些工具的IDE开发环境Eclipse-CDT。运行环境和开发环境既可以在Linux或Windows中使用。 8 | 9 | 10 | -------------------------------------------------------------------------------- /zh/preface/summary_preface.md: -------------------------------------------------------------------------------- 1 | 本章比较概要地介绍了操作系统运行的计算机硬件架构,包括CPU、内存和外设,并对操作系统的历史发展、定义、目标、接口、抽象和特征等进行了阐述。最后简要介绍了课程实验用到的ucore操作系统。 2 | 3 | -------------------------------------------------------------------------------- /zh/preface/ucore.md: -------------------------------------------------------------------------------- 1 | # ucore简介 2 | 3 | ucore目前支持的硬件环境是基于Intel 80386以上的计算机系统。更多的硬件相关内容(比如保护模式等)将随着实现ucore的过程逐渐展开介绍。那我们准备如何一步一步实现ucore呢?安装一个操作系统的开发过程,我们可以有如下的开发步骤: 4 | 5 | 1. bootloader+toy ucore:理解操作系统启动前的硬件状态和要做的准备工作,了解运行操作系统的外设硬件支持,操作系统如何加载到内存中,理解两类中断--“外设中断”,“陷阱中断”,内核态和用户态的区别; 6 | 2. 物理内存管理:理解x86分段/分页模式,了解操作系统如何管理物理内存; 7 | 3. 虚拟内存管理:理解OS虚存的基本原理和目标,以及如何结合页表+中断处理(缺页故障处理)来实现虚存的目标,如何实现基于页的内存替换算法和替换过程; 8 | 4. 内核线程管理:理解内核线程创建、执行、切换和结束的动态管理过程,以及内核线程的运行周期等; 9 | 5. 用户进程管理:理解用户进程创建、执行、切换和结束的动态管理过程,以及在用户态通过系统调用得到内核中各种服务的过程; 10 | 6. 处理器调度:理解操作系统的调度过程和调度算法; 11 | 7. 同步互斥与进程间通信:理解同步互斥的具体实现以及对系统性能的影响,研究死锁产生的原因,如何避免死锁,以及线程/进程间如何进行信息交换和共享; 12 | 8. 文件系统:理解文件系统的具体实现,与进程管理和内存管理等的关系,缓存对操作系统IO访问的性能改进,虚拟文件系统(VFS)、buffer cache和disk driver之间的关系。 13 | 14 | 其中每个开发步骤都是建立在上一个步骤之上的,就像搭积木,从一个一个小木块,最终搭出来一个小房子。在搭房子的过程中,完成从理解操作系统原理到实践操作系统设计与实现的探索过程。这个房子最终的建筑架构和建设进度如下图所示 15 | > (!可进一步标注处各个proj在下图中的位置) 16 | 17 | ![ucore操作系统架构](figures/ucore_arch.png) 18 | -------------------------------------------------------------------------------- /zh/preface/understandos.md: -------------------------------------------------------------------------------- 1 | 我们可以把软件分成应用软件和系统软件,而对于系统软件,我们又可以分为系统应用和操作系统。操作系统是一个大型复杂软件,但如果从使用和实现的抽象角度来看,还是可以用一些比较简洁的描述对其定义、特征等进行归纳总结,让读者有一个比较宽泛的总体理解。 2 | 3 | 4 | 5 | ![](/zh/chapter-1/figures/os-position.png) 6 | 7 | -------------------------------------------------------------------------------- /zh/supplement/copyright.md: -------------------------------------------------------------------------------- 1 | # 版权信息 2 | ucore OS是用于清华大学计算机系本科操作系统课程的OS教学试验内容。 3 | ucore OS起源于MIT CSAIL PDOS课题组开发的xv6&jos、哈佛大学开发的 4 | OS161教学操作系统、以及Linux-2.4内核。 5 | 6 | ucore OS中包含的xv6&jos代码版权属于Frans Kaashoek, Robert Morris, 7 | and Russ Cox,使用MIT License。ucore OS中包含的OS/161代码版权属于 8 | David A. Holland。其他代码版权属于陈渝、王乃峥、向勇,并采用GPL License. 9 | ucore OS相关的文档版权属于陈渝、向勇,并采用 Creative Commons 10 | Attribution/Share-Alike (CC-BY-SA) License. ** 11 | -------------------------------------------------------------------------------- /zh/supplement/mooc-os.md: -------------------------------------------------------------------------------- 1 | # MOOC OS 相关资料 2 | 3 | ## OS基本概念和原理 4 | 5 | * MOOC OS 2015 on 学堂在线 https://www.xuetangx.com/courses/TsinghuaX/30240243X/2015_T1/about 6 | * MOOC OS 2014 on TOPU http://www.topu.com/mooc/4100 7 | 8 | ## OS设计与实现细节 9 | * OS实验代码 https://github.com/chyyuu/mooc_os_lab 10 | 11 | ## 动手实践OS 12 | 13 | - "操作系统简单实现与基本原理 — 基于ucore" http://chyyuu.gitbooks.io/ucorebook/ 14 | - "操作系统简单实现与基本原理 — 基于ucore" 配套代码 https://github.com/chyyuu/ucorebook_code 15 | - ucore plus 跨硬件平台的ucore OS https://github.com/chyyuu/ucore_plus 16 | 17 | ## MOOC OS 2015 WIKI 18 | - http://os.cs.tsinghua.edu.cn/oscourse/OS2015 19 | 20 | 21 | ## 在线交流 22 | 23 | - [清华计算机系MOOC OS课程在线QA平台](https://piazza.com/tsinghua.edu.cn/spring2015/30240243x/home) 24 | - QQ群 181873534 主要用于事件通知,聊天等 25 | 26 | ## 课程汇总信息 27 | 28 | - [课程汇总](https://github.com/chyyuu/mooc_os) 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /zh/supplement/supplement.md: -------------------------------------------------------------------------------- 1 | # 附录 2 | -------------------------------------------------------------------------------- /zh/supplement/ucore-contributors.md: -------------------------------------------------------------------------------- 1 | ### 开发维护人员 2 | * 当前维护者 3 | * 陈渝 http://soft.cs.tsinghua.edu.cn/~chen yuchen@tsinghua.edu.cn 4 | * 茅俊杰 eternal.n08@gmail.com 5 | * 向勇 xyong@tsinghua.edu.cn 6 | * 贡献者 7 | 8 | 茅俊杰、陈宇恒、刘聪、杨扬、渠准、任胜伟、朱文雷、 9 | 曹正、沈彤、陈旭、蓝昶、方宇剑、韩文涛、张凯成、 10 | S郭晓林、薛天凡、胡刚、刘超、粟裕、袁昕颢... 11 | 12 | 13 | -------------------------------------------------------------------------------- /zh/supplement/ucore-history.md: -------------------------------------------------------------------------------- 1 | # ucore历史 2 | --------- 3 | 写一个教学OS的初衷是陈渝老师和向勇老师想参考MIT的xv6/JOS开发一个能够与OS课程教材相配套的OS实验环境。没有直接采用xv6/JOS的原因是当时(2008年)xv6没有完整的保护模式页机制和虚存管理机制,JOS不是传统的UNIX 单体内核架构,而是Exokerne内核架构,与当前OS教学的知识点有点远,在互联网上找了一圈,没有合适的。有人说为何不用Linux?其实Linux确实挺好的,只是对于首次学习OS原理的本科生要在短短一学期内搞懂Linux的部分实现细节,可能付出的代价会比较大,需要冒着挂掉其他课的风险。为此陈渝老师鼓励他带的硕士研究生王乃峥试试能否仿照xv6和linux自己鼓捣一个教学用的小OS,并用Ken Thompson和Linus在短短3个月分别开发了UNIX和Linux的故事来从精神上激励他。王乃崢同学看了xv6的代码,本着试试看的想法,就开始coding,并查看各种相关文档和资料,发现也只花了短短一个月不到的时间就完成了支持lab1实验的ucore OS;为此信心大增,以月为单位又接连完成了支持lab2~lab8的ucore OS,前后大约花了8个月(这8个月还顺便完成了减轻体重和找女朋友的重要工作)。做完此事后,王乃峥同学离毕业只有3个时间了。有了之前OS开发的底子,他在3个月的时间内,完成了Linux kernel相关的硕士课题,顺利毕业,开始了他的创业生涯。 4 | 5 | 在此之后,茅俊杰同学接手了ucore的进一步改进,在他直博期间长期担任操作系统课程的助教,他做的一些有意思包括,扩展ucore-plus(ucore加强版)支持用户态ucore的设计实现(课程大实验),移植到mips/openrisc等CPU上等。目前已经顺利毕业,开始了在产业界的工作。再后来,又有不少同学进行了尝试,比如最近(2017年)张蔚同学把ucore移植到了开源的RISC-V32 CPU上,并准备在2018年做助教辅导他的同班同学。 6 | 7 | 而向勇老师和陈渝老师鼓励和引导后续的学生继续着操作系统教学和科研的快乐之旅。茅俊杰、陈宇恒、刘聪、杨扬、渠准、任胜伟、朱文雷、曹正、沈彤、陈旭、蓝昶、方宇剑、韩文涛、张凯成、郭晓林、薛天凡、胡刚、刘超、粟裕、袁昕颢、张宇翔、王邈、张蔚、雷凯翔等同学在他们学习OS课程和实践ucore OS的过程中给老师们留下了深刻的印象。目前发现ucore中有不少的bug(不过少于Linux的bug),陈渝老师准备带着学生再研究一些算法、方法和工具,能够在ucore运行前通过静态分析的方法发现其潜在的bug,而且希望能够在ucore在编译时或漰溃时,找到内核代码的bug在哪里,并能分析出为何这个内核代码会导致ucore漰溃的因果链(如果有人感兴趣做这方面的尝试,欢迎直接电邮/电话陈渝老师!)。希望这样能够减轻大家学习OS实验的负担。 8 | 9 | -------------------------------------------------------------------------------- /zh/supplement/ucore-tools.md: -------------------------------------------------------------------------------- 1 | # ucore实验中的常用工具 2 | 3 | 在ucore实验中,一些基本的常用工具如下: 4 | - 命令行shell: bash shell -- 有对文件和目录操作的各种命令,如ls、cd、rm、pwd... 5 | - 系统维护工具:apt、git 6 | - apt:安装管理各种软件,主要在debian, ubuntu linux系统中 7 | - git:开发软件的版本维护工具 8 | - 源码阅读与编辑工具:eclipse-CDT、understand、gedit、vim 9 | - Eclipse-CDT:基于Eclipse的C/C++集成开发环境、跨平台、丰富的分析理解代码的功能,可与qemu结合,联机源码级Debug uCore OS。 10 | - Understand:商业软件、跨平台、丰富的分析理解代码的功能,Windows上有类似的sourceinsight软件 11 | - gedit:Linux中的常用文本编辑,Windows上有类似的notepad 12 | - vim: Linux/unix中的传统编辑器,类似有emacs等,可通过exuberant-ctags、cscope等实现代码定位 13 | - 源码比较工具:diff、meld,用于比较不同目录或不同文件的区别 14 | - diff是命令行工具,使用简单 15 | - meld是图形界面的工具,功能相对直观和方便,类似的工具还有 kdiff3、diffmerge、P4merge 16 | - 开发编译调试工具:gcc 、gdb 、make 17 | - gcc:C语言编译器 18 | - gdb:执行程序调试器 19 | - ld:链接器 20 | - objdump:对E执行程序文件进行反编译、转换执行格式等操作的工具 21 | - nm:查看执行文件中的变量、函数的地址 22 | - readelf:分析ELF格式的执行程序文件 23 | - make:软件工程管理工具, make命令执行时,需要一个 makefile 文件,以告诉make命令如何去编译和链接程序 24 | - dd:读写数据到文件和设备中的工具 25 | - 硬件模拟器:qemu -- qemu可模拟多种CPU硬件环境,本实验中,用于模拟一台 intel x86-32的计算机系统。类似的工具还有BOCHS, SkyEye等 26 | 27 | 28 | # 上述工具的使用方法在线信息 29 | - apt-get 30 | - http://wiki.ubuntu.org.cn/Apt-get%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97 31 | - git 32 | - http://www.cnblogs.com/cspku/articles/Git_cmds.html 33 | - gcc 34 | - http://wiki.ubuntu.org.cn/Gcchowto 35 | - http://wiki.ubuntu.org.cn/Compiling_Cpp 36 | - http://wiki.ubuntu.org.cn/C_Cpp_IDE 37 | - http://wiki.ubuntu.org.cn/C%E8%AF%AD%E8%A8%80%E7%AE%80%E8%A6%81%E8%AF%AD%E6%B3%95%E6%8C%87%E5%8D%97 38 | - gdb 39 | - http://wiki.ubuntu.org.cn/%E7%94%A8GDB%E8%B0%83%E8%AF%95%E7%A8%8B%E5%BA%8F 40 | - make & makefile 41 | - http://wiki.ubuntu.com.cn/index.php?title=%E8%B7%9F%E6%88%91%E4%B8%80%E8%B5%B7%E5%86%99Makefile&variant=zh-cn 42 | - http://blog.csdn.net/a_ran/article/details/43937041 43 | - shell 44 | - http://wiki.ubuntu.org.cn/Shell%E7%BC%96%E7%A8%8B%E5%9F%BA%E7%A1%80 45 | - http://wiki.ubuntu.org.cn/%E9%AB%98%E7%BA%A7Bash%E8%84%9A%E6%9C%AC%E7%BC%96%E7%A8%8B%E6%8C%87%E5%8D%97 46 | - understand 47 | - http://blog.csdn.net/qwang24/article/details/4064975 48 | - vim 49 | - http://www.httpy.com/html/wangluobiancheng/Perljiaocheng/2014/0613/93894.html 50 | - http://wenku.baidu.com/view/4b004dd5360cba1aa811da77.html 51 | - meld 52 | - https://linuxtoy.org/archives/meld-2.html 53 | - qemu 54 | - http://wenku.baidu.com/view/04c0116aa45177232f60a2eb.html 55 | - Eclipse-CDT 56 | - http://blog.csdn.net/anzhu_111/article/details/5946634 57 | 58 | --------------------------------------------------------------------------------