HIT深入理解計算機系統大作業

計算機系統大作業

題 目 : 程序人生-Hello’sP2P

專 業: 計算機科學與技術

學  號: 1180300422

班  級: 1803004

學 生: 吳仁龍

指 導 教 師: 史先俊

計算機科學與技術學院

2019年12月

摘 要

本文通過分析hello.c從誕生到死亡的全過程,包括預處理、編譯、彙編、鏈接、在進程中執行及被銷燬被操作系統回收,較全面的回答了計算機如何操控大局“軟”“硬”結合完成程序的執行的問題,內容涉及彙編、鏈接、存儲管理、進程管理、IO管理等。

關鍵詞:hello.c;程序;編譯;鏈接;進程;

目 錄

第1章
概述… - 4 -

1.1 Hello簡介… - 4 -

1.2 環境與工具… - 4 -

1.3 中間結果… - 4 -

1.4 本章小結… - 4 -

第2章
預處理… - 6 -

2.1 預處理的概念與作用… - 6 -

2.2在Ubuntu下預處理的命令… - 6 -

2.3 Hello的預處理結果解析… - 7 -

2.4 本章小結… - 7 -

第3章
編譯… - 8 -

3.1 編譯的概念與作用… - 8 -

3.2 在Ubuntu下編譯的命令… - 8 -

3.3 Hello的編譯結果解析… - 8 -

3.4 本章小結… - 11 -

第4章
彙編… - 12 -

4.1 彙編的概念與作用… - 12 -

4.2 在Ubuntu下彙編的命令… - 12 -

4.3 可重定位目標elf格式… - 12 -

4.4 Hello.o的結果解析… - 14 -

4.5 本章小結… - 15 -

第5章
鏈接… - 16 -

5.1 鏈接的概念與作用… - 16 -

5.2 在Ubuntu下鏈接的命令… - 16 -

5.3 可執行目標文件hello的格式… - 16 -

5.4 hello的虛擬地址空間… - 18 -

5.5 鏈接的重定位過程分析… - 18 -

5.6 hello的執行流程… - 19 -

5.7 Hello的動態鏈接分析… - 20 -

5.8 本章小結… - 21 -

第6章
hello進程管理… - 22 -

6.1 進程的概念與作用… - 22 -

6.2 簡述殼Shell-bash的作用與處理流程… - 22 -

6.3 Hello的fork進程創建過程… - 22 -

6.4 Hello的execve過程… - 23 -

6.5 Hello的進程執行… - 23 -

6.6 hello的異常與信號處理… - 24 -

6.7本章小結… - 27 -

第7章
hello的存儲管理… - 28 -

7.1 hello的存儲器地址空間… - 28 -

7.2 Intel邏輯地址到線性地址的變換-段式管理… - 28 -

7.3 Hello的線性地址到物理地址的變換-頁式管理… - 29 -

7.4 TLB與四級頁表支持下的VA到PA的變換… - 30 -

7.5 三級Cache支持下的物理內存訪問… - 31 -

7.6 hello進程fork時的內存映射… - 32 -

7.7 hello進程execve時的內存映射… - 32 -

7.8 缺頁故障與缺頁中斷處理… - 33 -

7.9動態存儲分配管理… - 34 -

7.10本章小結… - 36 -

第8章
hello的IO管理… - 37 -

8.1 Linux的IO設備管理方法… - 37 -

8.2 簡述Unix IO接口及其函數… - 37 -

8.3 printf的實現分析… - 38 -

8.4 getchar的實現分析… - 39 -

8.5本章小結… - 39 -

結論… - 40 -

附件… - 41 -

參考文獻… - 42 -

第1章 概述

1.1 Hello簡介

P2P:在Linux下,Hello.c經過預處理、編譯、彙編、鏈接生成可執行文件Hello,在shell中輸入執行命令後,shell通過OS進程管理爲其fork產生子進程,在該過程中,Hello.c從程序(Program)實現成進程(Process),即From Program to
Process,簡稱P2P。

020:在P2P之後,shell通過OS進程管理execve加載執行Hello進程,映射虛擬內存,進入程序入口後程序載入物理內存,CPU爲Hello分配時間片一執行邏輯控制流,在程序結束後,shell回收Hello進程,刪除其相關存儲,Hello結束生命,整個過程From Zero-0 to
Zero-0,簡稱020。

1.2 環境與工具

列出你爲編寫本論文,折騰Hello的整個過程中,使用的軟硬件環境,以及開發與調試工具。

硬件環境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上

軟件環境:Win10

開發工具:Visual Studio
2019;VMware;Ubuntu;edb;gcc;

1.3 中間結果

列出你爲編寫本論文,生成的中間結果文件的名字,文件的作用等。

hello.c:hello源程序

hello.i:hello.c預處理產生的ASCII文件

hello.s:hello.i編譯產生的彙編代碼文件

hello.o:彙編後產生的可重定位文件

hello:可重定位目標文件鏈接後的可執行文件

1.4 本章小結

本節粗略描述了一個hello程序從誕生到死亡的過程及在描述整個過程時所使用的軟件環境和生成的中間文件。

第2章 預處理

2.1 預處理的概念與作用

預處理器(cpp)會將以字符#開頭的命令試圖解釋爲預處理指令,修改原始的c程序。其中ISO C/C++要求支持的包括#if/#ifdef/#ifndef/#else/#elif/#endif(條件編譯)、#define(宏定義)、#include(源文件包含)、#line(行控制)、#error(錯誤指令)、#pragma(和實現相關的雜注)以及單獨的#(空指令)。預處理指令一般被用來使源代碼在不同的執行環境中被方便的修改或者編譯。比如,hello.c 中的第一行#include <stdio.h>命令告訴預處理器讀取系統頭文件stdio.h中的內容,並把它直接插入程序文本中,結果就得到了另一個c程序,通常以.i作爲文件擴展名。

2.2在Ubuntu下預處理的命令

預處理命令行:gcc -E hello.c -o hello.i 生成hello.i文件
在這裏插入圖片描述

2.3 Hello的預處理結果解析

hello.c預處理之後生成hello.i文件,gedit hello.i打開文件:
在這裏插入圖片描述

源程序中hello.c的程序被放在了hello.i的文件末尾,預處理器將頭文件stdio.h、unistd.h、stdlib.h依次展開,若在展開該頭文件的過程中仍然遇到了以#開頭的define,預處理器會對此繼續展開。

2.4 本章小結

   在預處理階段,預處理器按一定規則解析以#開頭的預處理指令,向helo.i文件中引入系統文件等,爲程序的進一步編譯做好準備。

第3章 編譯

3.1 編譯的概念與作用

在編譯階段,編譯器檢查是否有語法錯誤,檢查無誤後,編譯器將.i文件翻譯成.s文件,它包含一個彙編語言程序,該程序包含main函數的定義。彙編語言程序爲不同高級語言的不同編譯器提供了通用的輸出語言,爲低級機器語言。

3.2 在Ubuntu下編譯的命令

編譯命令:gcc -S hello.i -o hello.s 生成hello.s 文件
在這裏插入圖片描述

3.3 Hello的編譯結果解析

3.3.1主函數

主函數main被解析成全局函數,其中使用的字符串常量也被放置在數據區。
在這裏插入圖片描述

Main函數有兩個參數,分別爲有符號數argc和字符型數組指針argv,根據寄存器使用規則,這兩個參數分別通過%edi和%esi傳遞。在程序最開始,爲main函數建立棧幀,並完成參數傳遞。
在這裏插入圖片描述

3.3.2 賦值操作

對於局部變量i,源程序中有賦值爲0的操作,在hello.s文件中通過mov語句實現。
在這裏插入圖片描述

3.3.3 類型轉換

Sleep函數的參數爲int值,而argv爲字符串數組,在hello.c中用atoi將字符串轉化成int型,在hello.s中用call語句調用atoi函數強制處理該類型轉換。
在這裏插入圖片描述

3.3.4 關係操作

在hello.c中癡線了兩處關係操作,第一處是argc!=4,判斷用戶鍵入的參數個數是否是4,另一處是i<8,判斷i的值是否小於8,來決定循環要不要繼續。在hello.s中都通過cmp語句來判斷關係。
在這裏插入圖片描述
在這裏插入圖片描述

在比較i和8的關係的時候,編譯器並沒有和8比較,而是和7,結合程序功能,當i>=8時結束循環,和當i<=7時繼續循環是等價概念(i初值爲0,且只增不減)。

3.3.5 算術操作

在hello.c文件中通過i++語句實現對i值的自增,且自增步長爲1,在hello.s文件中通過add語句實現該運算。
在這裏插入圖片描述

3.3.6 數組/指針/結構操作

在hello.c中,字符串指針數組argv,多次引用argv[1]、argv[2]、argv[3],分別存儲着學號、姓名及秒數。

傳遞argv[2]:
在這裏插入圖片描述

傳遞argv[1]:
在這裏插入圖片描述

傳遞argv[3]:
在這裏插入圖片描述

3.3.7 控制轉移

在hello.c中有兩處有關轉移,一處是if語句實現的轉移,一處是for語句實現的轉移。

If語句在判斷argc等於4之後,執行相應的功能語句,在hello.s中用je語句實現:
在這裏插入圖片描述

For語句在判斷i仍然小於8後,繼續轉到循環起始處執行,在hello.s中用jle實現:
在這裏插入圖片描述

3.3.8 函數操作

在hello.c中多處涉及函數操作,調用另一個函數來執行當前任務,如printf、atoi、getchar、sleep、exit,在hello.s中對此的處理均是先完成參數傳遞(有入口參數的情況下),然後在用call語句轉到相應函數的入口處執行。

printf,用%rdi傳參:
在這裏插入圖片描述
exit,用%edi傳參:
在這裏插入圖片描述
atoi,用%rdi傳參:
在這裏插入圖片描述
Sleep,用%edi傳參:
在這裏插入圖片描述

3.4 本章小結

編譯器通過詞法分析和語法分析,來檢查原始代碼有沒有錯誤,在確認沒有錯誤之後,編譯器會按照一定的規範生成與原始代碼等價的中間代碼或彙編代碼,在這個過程中,編譯器可能會按照自己的理解(一系列算法),對原始代碼結構和數據做出調整。

第4章 彙編

4.1 彙編的概念與作用

   在該階段,彙編器(as)通過彙編程序將hello.s翻譯成機器語言指令,把這些指令打包成一種叫做可重定位目標程序的格式,並將結果保存在hello.o文件中,實現從彙編程序到機器指令的轉換。Hello.o文件是一個二進制文件,包含程序的指令編碼。彙編語言的指令與機器語言的指令大體上保持一一對應的關係,彙編算法採用的基本策略是簡單的。通常採用兩遍掃描源程序的算法。第一遍掃描源程序根據符號的定義和使用,收集符號的有關信息到符號表中;第二遍利用第一遍收集的符號信息,將源程序中的符號化指令逐條翻譯爲相應的機器指令。

4.2 在Ubuntu下彙編的命令

   彙編命令:gcc -c hello.s -o hello.o

在這裏插入圖片描述

4.3 可重定位目標elf格式

打開ELF命令行:readelf -a
hello.o

ELF文件從ELF頭開始,ELF頭以一個16個字節的序列開始,這個序列描述了生成該文件的系統的字大小和字節順序。在該ELF頭中顯示,以小端碼機器存儲,文件類型爲可重定位目標文件。
在這裏插入圖片描述

接着是節頭。節頭描述了目標文件中每一個節的位置和大小,每個節都有一個固定的條目,包括名稱、類型、地址及偏移量等。
在這裏插入圖片描述

然後看重定位節。當彙編器生成一個目標模塊時,它並不知道數據和代碼最終將存放在存儲器中的什麼位置。它也不知道這個模塊引用的任何外部定義的函數和全局變量。所以,無論何時彙編器遇到對最終位置未指定目標引用,它就會生成一個重定位條目,告訴鏈接器在將目標文件合併可執行文件時如何修改這個引用。代碼重定位條目放在.rel.text中。已經初始化數據的重定位條目放在.rel.data中。重定位條目中描述了需重定位符號的偏移量、信息、類型、符號值及名稱等。

重定位條目的格式可用以下數據結構描述:

typedef struct{
	long offset;//offset of the reference to locate
	long type:32;//relocation type
		symlol :32;//symbol table index
	long attend;//constant part of relocation expression
}ELF64_Rela

被修改的引用的節偏移;type描述了重定位類型,有兩種最基本的重定位類型:R_X86_64_PC32和R_X86_64_32,R_X86_64_PC32重定位一個使用32位PC相對地址的引用,R_X86_64_32重定位一個使用32位絕對地址的引用;symbol標識了被修改引用應指向的符號;addend爲一個符號常數,表示對修改引用的值做出的偏移調整。
在這裏插入圖片描述

最後,看.symtab節。這是一個符號表,存放着在程序中定義和引用的函數和全局變量的信息。
在這裏插入圖片描述

4.4 Hello.o的結果解析

反彙編命令行:objdump -d -r hello.o
在這裏插入圖片描述

與hello.s對比,主要差異如下:

  1. hello.s中call語句後緊跟着函數名,而在hello.o反彙編中call語句後跟着的是相對地址,顯示相應的重定位條目,因爲未鏈接無法確定絕對地址。

  2. hello.s中跳轉語句後緊跟着的是.L2.L3這樣的標籤,而在hello.o反彙編中跳轉語句後跟着的是相對地址。

  3. hello.s中的操作數是十進制,而在hello.o反彙編中操作數是十六進制。

4.5 本章小結

  在彙編階段,彙編器生成與彙編代碼對應的機器指令,並處理分配ELF文件各節的信息,爲鏈接生成可執行文件做好準備。

第5章 鏈接

5.1 鏈接的概念與作用

鏈接(linking)是將各種代碼和數據片段收集並組合成爲一個單一文件的過程,這個文件可被加載(複製)到內存中並執行。鏈接可以中興於編譯時、也就是源代碼被翻譯成機器代碼時;也可以執行於加載時,也就是在程序被加載器加載到內存並執行時;甚至執行於運行時。也就是由應用程序來執行。

鏈接主要完成兩個任務:符號解析和重定位。符號解析將每個符號引用正好和一個符號定義關聯起來;重定位將每個符號定義與一個內存位置關聯起來,修改所有對這些符號的引用,使得他們指向這個內存位置。

5.2 在Ubuntu下鏈接的命令

鏈接命令行:ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o
在這裏插入圖片描述
5.3 可執行目標文件hello的格式

可執行目標文件hello的格式類似於可重定位目標文件hello.o的格式。ELF頭描述文件的總體格式。它還包括程序的入口點,也就是當程序運行時要執行的第一條指令的地址。.text、.rodata和.data節與可重定位目標文件的節是相似的,除了這些節已經被重定位到它們最終的運行時內存外。.init節定義了一個小函數,叫_init,程序的初始化會調用它。因爲可執行文件hello是完全鏈接的,所以不再需要.rel節。
在這裏插入圖片描述

5.4 hello的虛擬地址空間

在5.3節中包含各節在ELF文件中的偏移信息,比如.text的偏移量爲0x00000550,則.text節的位置爲0x400550:
在這裏插入圖片描述

再比如.rodata節的偏移爲0x690,則.rodata節的位置爲0x400690:
在這裏插入圖片描述

5.5 鏈接的重定位過程分析

較hello反彙編結果和hello.o反彙編結果,不同之處主要在於:

  1. 在hello.o中call、jmp指令後緊跟着的是相對地址,而hello中緊跟的是虛擬內存的確定地址,原因在於鏈接器完成了重定位過程,可以確定運行時的地址

  2. 在hello中增加了一些在hello.o中沒有的函數,這些都是在hello.c中沒有定義卻直接使用的函數,這些函數定義在共享庫中,在鏈接時完成了符號解析和重定位,如printf、sleep等。

下面結合call sleep來解釋hello中是怎麼對其重定位的:
在hello.o的反彙編中可以看到,call sleep語句是這麼寫的:
在這裏插入圖片描述

再去查看hello.o的ELF中有關sleep的可重定位條目:
在這裏插入圖片描述

Offset=0x74,symbol=sleep,type=0xR_X86_64_PLT32,addend=-4

這些信息告訴鏈接器修改開始於偏移量0x74處的32位PLT相對引用,這樣在運行時會指向sleep的例程,接着鏈接器會按照0xR_X86_64_PLT32重定位類型具體的規定計算出相應的數值。

對於R_X86_64_PLT32類型的引用是動態鏈接的,也就是在靜態鏈接過程中只是簡單的構造過程鏈接表(PLT)和全局偏移量表(GOT),然後在程序加載到內存裏運行的過程中才會完成最終的重定位工作。
在這裏插入圖片描述

5.6 hello的執行流程

(Edb命令行:./edb --run
/mnt/hgfs/hitics/CS_Hello/hello 1180300422 吳仁龍 1)

ld-2.27.so!_dl_start

ld-2.27.so!_dl_init

hello!_start

ld-2.27.so!_dl_start_main

ld-2.27.so!_cxa_atexit

hello!_libc_csu_init

libc-2.27.so!setjump

hello!printf@plt

hello!atoi@plt

hello!sleep@plt

hello!getchar@plt

hello!exit@plt

5.7 Hello的動態鏈接分析

通過查詢hello的ELF文件,得.GOT.PLT的地址爲0x601000
在這裏插入圖片描述
在運行dl_start和dl_init之前,GOTPLT表的內容如圖所示:
在這裏插入圖片描述
在運行dl_start和dl_init之後,GOTPLT表的內容如圖所示:
在這裏插入圖片描述

動態鏈接是一項有趣的技術。考慮一個簡單的事實,printf,getchar這樣的函數實在使用的太過頻繁,因此如果每個程序鏈接時都要將這些代碼鏈接進去的話,一份可執行目標文件就會有一份printf的代碼,這是對內存的極大浪費。爲了遏制這種浪費,對於這些使用頻繁的代碼,系統會在可重定位目標文件鏈接時僅僅創建兩個輔助用的數據結構,而直到程序被加載到內存中執行的時候,纔會通過這些輔助的數據結構動態的將printf的代碼重定位給程序執行。即是說,直到程序加載到內存中運行時,它才知曉所要執行的代碼被放在了內存中的哪個位置。這種技術被稱爲延遲綁定,將過程地址的綁定推遲到第一次調用該過程時。而那兩個輔助的數據結構分別是過程鏈接表(PLT)和全局偏移量表(GOT),前者存放在代碼段,後者存放在數據段。

5.8 本章小結

鏈接技術通過符號解析和重定位完成實現將各種代碼和數據片段收集並組合成一個單一文件的功能,該技術使得分離編譯成爲可能,爲大型軟件的模塊化開發和維護奠定了基礎。

第6章 hello進程管理

6.1 進程的概念與作用

進程的經典定義就是一個執行中程序的實例。系統中的每個程序都運行在進程的上下文中。上下文是由程序正確運行所需的狀態組成的。這個狀態包括存放在內存中的程序的代碼和數據,它的棧、通用目的寄存器的內容、程序計數器、環境變量以及打開文件描述符的集合。進程提供給應用程序的關鍵抽象如下:一個獨立的邏輯控制流和一個私有的地址空間。

6.2 簡述殼Shell-bash的作用與處理流程

shell是一個交互型應用級程序,其基本功能是解釋並執行用戶打入的各種命令,實現用戶與Linux核心的接口。系統初啓後,核心爲每個終端用戶建立一個進程去執行Shell解釋程序。它的執行過程基本上按如下步驟: (1)讀取用戶由鍵盤輸入的命令行。 (2)分析命令,以命令名作爲文件名,並將其它參數改造爲系統調用execve( )內部處理所要求的形式。 (3)終端進程調用fork( )建立一個子進程。 (4)終端進程本身用系統調用wait4( )來等待子進程完成(如果是後臺命令,則不等待)。當子進程運行時調用execve( ),子進程根據文件名(即命令名)到目錄中查找有關文件(這是命令解釋程序構成的文件),將它調入內存,執行這個程序(解釋這條命令)。 (5)如果命令末尾有&號(後臺命令符號),則終端進程不用系統調用wait4( )等待,立即發提示符,讓用戶輸入下一個命令,轉⑴。如果命令末尾沒有&號,則終端進程要一直等待,當子進程(即運行命令的進程)完成處理後終止,向父進程(終端進程)報告,此時終端進程醒來,在做必要的判別等工作後,終端進程發提示符,讓用戶輸入新的命令,重複上述處理過程。

6.3 Hello的fork進程創建過程

父進程可以通過fork函數創建一個新的運行的子進程,其函數聲明爲:

pid_t fork(void),子進程享有與父進程相同但各自獨立的上下文,包括代碼、堆、數據段、共享庫以及用戶棧。

在父進程中,fork函數返回子進程的PID,在子進程中,fork函數返回0.

當我們在終端中輸入./hello時,shell會先判斷髮現這個參數並不是Shell內置的命令,於是就把這條命令當作一個可執行程序的名字,它的判斷顯然是對的。

接下了shell會執行fork函數爲hello創建進程。

6.4 Hello的execve過程

當前進程可通過execve函數在自己的上下文中加載並運行一個新程序,其函數聲明爲:int execve(const char *filename,const char *argv[],const char *envp[]),

Execve函數加載並運行可執行目標文件filename,且帶參數列表argc和環境變量列表envp。只有當出現錯誤時,例如找不到filename,execve纔會返回到調用程序,所以,與fork函數一次調用返回兩次不同,execve調用一次從不返回。

在execve加載了hello之後,它會調用系統提供的啓動代碼,啓動代碼設置棧,系統會用execve構建的數據結構覆蓋其上下文,替換成hello的上下文,然後將控制傳遞給新程序的主函數main。

6.5 Hello的進程執行

結合進程上下文信息、進程時間片,闡述進程調度的過程,用戶態與核心態轉換等等。

進程爲每個程序提供一種假象,好像它在獨佔地使用處理器。事實上,CPU爲每個進程分配時間片,通過邏輯控制流不停的切換當前進程。每個進程執行它的流的一部分,然後被搶佔(暫時掛起),然後輪到其它進程。
在這裏插入圖片描述

操作系統內核使用上下文切換的較高層次的異常控制流來實現多任務。內核爲每個進程維護一個上下文,上下文由一些對象組成,通常包括通用目的寄存器、浮點寄存器、程序計數器、用戶棧和各種內核數據結構。在進程執行的某些時刻,內核可以決定搶佔當前進程,並重新開始一個先前被搶佔了的新的進程,這種決策稱爲調度。hello進程在內存中執行的過程中,也並不是一直佔用着cpu的資源。因爲當內核代表用戶執行系統調用時,可能會發生上下文切換,比如說hello中的sleep語句執行時,或者當Hello進程以及運行足夠久了的時候。每到這時,內核中的調度器就會執行上下文切換,將當前的上下文信息保存到內核中,恢復某個先前被搶佔的進程的上下文,然後將控制傳遞給這個新恢復的進程。再比如,假如hello中有讀寫磁盤的操作,就會發生下圖所示的調度:
在這裏插入圖片描述

6.6 hello的異常與信號處理

亂按鍵盤,正常執行:
在這裏插入圖片描述

按下ctrl+z,進程收到SIGSTP信號,暫時掛起:
在這裏插入圖片描述
使用ps查看進程:
在這裏插入圖片描述
使用jobs查看當前作業:
在這裏插入圖片描述

使用fg運行前臺進程:
在這裏插入圖片描述

使用pstree查看計算機當前正在執行的所有進程之間的關係:
在這裏插入圖片描述

按下ctrl+c,向進程發送終止信號SIGINT:
在這裏插入圖片描述

當某種異常發生時,就會向進程發送某種異常信號,進程接受到信號後就會轉到異常處理子程序處處理該異常。

6.7本章小結

進程是計算機科學中最深刻最成功的概念,獨立的邏輯控制流給我們提供我們的程序獨立的佔有處理器的假象,私有的地址空間給我們提供我們的程序獨立的使用內存系統的假象;異常的概念爲進程之間的溝通架起了橋樑,也爲軟件層面和硬件層面的溝通架起了橋樑。

第7章 hello的存儲管理

7.1 hello的存儲器地址空間

邏輯地址:指機器語言指令中,用來指定一個操作數或者是一條指令的地址。一個邏輯地址,是由一個段標識符加上一個指定段內相對地址的偏移量構成的。通俗的說:邏輯地址是給程序員設定的,底層代碼是分段式的,代碼段、數據段、每個段最開始的位置爲段基址,放在如CS、DS這樣的段寄存器中,再加上偏移,這樣構成一個完整的地址。

線性地址:地址空間是一個非負整數地址的有序集合,如果地址空間中的非負整數都是連續的,那麼就稱該地址空間是一個線性地址空間,線性地址是是邏輯地址到物理地址變換之間的中間層,程序代碼會產生邏輯地址,或者說是段中的偏移地址,加上相應段的基地址就生成了一個線性地址。

物理地址:計算機系統的主存被組織成一個由若干個連續的字節大小的單元組成的數組,每個字節都有唯一的確定的編號,這個編號稱爲物理地址。

虛擬地址:現代計算機不直接使用物理地址,而是使用一種稱爲虛擬地址的中間層地址。虛擬地址和物理地址間存在着映射關係,當CPU生成一個虛擬地址來訪問主存,這個虛擬地址通過地址翻譯轉換成對應的物理地址,進而去訪問真實的物理空間。

7.2 Intel邏輯地址到線性地址的變換-段式管理

程序代碼會產生邏輯地址,一個邏輯地址由兩部份組成,段標識符及段內偏移量。段標識符放在段描述表中,在保護方式下,在保護方式下,每個段由如下三個參數進行定義:段基地址(Base Address)、段界限(Limit)和段屬性(Attributes)。:

(1):段基地址規定線性地址空間中段的開始地址;

(2):段界限規定段的大小。

(3):段的屬性表示段的特性。例如,該段是否可被讀出或寫入,或者該段是否作爲一個程序來執行,以及段的特權級等。

下圖表示一個段如何從虛擬地址空間定位到線性地址空間。圖中BaseA等代表段基地址, LimitA等代表段界限。另外,段C接在段A之後,也即BaseC=BaseA+LimitA。
在這裏插入圖片描述

7.3 Hello的線性地址到物理地址的變換-頁式管理

分頁機制把線性地址空間和物理地址空間分別劃分爲大小相同的塊。這樣的塊稱之爲頁。通過在線性地址空間的頁與物理地址空間的頁之間建立的映射,分頁機制實現線性地址到物理地址的轉換。這種映射關係是通過一種叫做頁表的數據結構實現的,頁表就是一個頁表條目(PTE)的數組,其基本的組織結構如下:
在這裏插入圖片描述

CPU中的一個控制寄存器,頁表基址寄存器指向當前頁表,n位的虛擬地址由虛擬頁面偏移(VPO, n位)和虛擬頁號(VPN, n - p位)組成。MMU利用VPN來選擇適當的PTE(頁表條目),將頁表條目中的物理頁號(PPN)與虛擬地址的頁面偏移量(VPO)串聯起來,就得到相應的物理地址。

頁面命中時,CPU硬件執行的步驟如下:

  1. 處理器生成一個虛擬地址,並將其傳送給MMU

  2. MMU生成PTE地址,並從高速緩存/內存中請求得到它

  3. 高速緩存/內存向MMU返回PTE(即MMU 使用內存中的頁表生成PTE)

  4. MMU構造物理地址,將其傳送給高速緩存/主存

  5. 高速緩存/主存返回所請求的數據字給處理器

頁面不命中時,CPU硬件執行的步驟如下:

  1. 處理器生成一個虛擬地址,並將其傳送給MMU

  2. MMU生成PTE地址,並從高速緩存/內存中請求得到它

  3. 高速緩存/內存向MMU返回PTE(即MMU 使用內存中的頁表生成PTE)

  4. PTE中的有效位爲零, 因此 MMU觸發缺頁異常

  5. 缺頁處理程序確定物理內存中犧牲頁 (若頁面被修改,則換出到磁盤)

  6. 缺頁處理程序調入新的頁面,並更新內存中的PTE

  7. 缺頁處理程序返回到原來進程,再次執行導致缺頁的指令

7.4 TLB與四級頁表支持下的VA到PA的變換

多級頁表的設計的初衷是爲了減少頁表內存駐留過大的問題:

假如有一個32位的地址空間,4KB的頁面和一個4字節的PTE,那麼即使應用所引用的只是虛擬地址空間中很小的一部分,也總是需要4MB的頁表駐留在內存中。

一級頁表指向二級頁表,二級頁表指向三級頁表,以此類推。如果片i中每個頁面都未分配,那麼一級PTEi就爲空,如果片i中至少有一個頁是分配了的,那麼一級PTEi就指向一個二級頁表的基址,其結構示意圖如下:
在這裏插入圖片描述

MMU將虛擬地址VA做處理,取出VPN1、VPN2、VPN3、VPN4(VPNi爲指向第i級頁表的索引)及VPO(虛擬頁面偏移量)。接着用VPN1在一級頁表中匹配,若匹配PTE不爲空,則用一級頁表PTE的內容到二級頁表中繼續匹配;若匹配爲空,則代表該頁未分配,產生缺頁,需要跳轉至缺頁處理子程序處理。在訪問完4個頁表之後,獲得物理頁面的PPN,再配合PPO(與VPO相等),可以獲得物理地址VP。

7.5 三級Cache支持下的物理內存訪問

MMU根據VP解析出相應的PP後,需要用PP在cache中尋找相應的數據,

根據第一級Cache的相關參數解析PP的索引位,具體說來,根據B大小解析PP的低b位作爲塊內偏移,根據S的大小解析緊跟着的s位作爲組索引,剩下的位全部作爲tag標記位,即如下圖所示:
在這裏插入圖片描述

接着根據這些索引位在Cache中匹配,若匹配上,根據匹配的結果在第二級Cache中重複類似上述操作,最終,第三級Cache中存放着相應的所需數據。如果沒有在cache中找到,就會發生不命中,此時會從下一級存儲中加載相應的塊到Cache中完成訪問。

7.6 hello進程fork時的內存映射

內存映射:Linux通過將一個虛擬內存區域與一個磁盤上的對象關聯起來,以初始化這個內存區域的內容,這個過程稱爲內存映射。一個虛擬頁面一旦被初始化了,它就在一個由內核維護的專門的交換文件之間換來換去。交換文件也叫交換空間或者交換區域,在任何時刻,交換空間都限制着當前運行着的進程能夠分配的虛擬頁面的總數。

當fork函數被當前進程調用時,內核爲新進程創建各種數據結構,並分配給它一個唯一的PID。爲了給這個新進程創建虛擬內存,它創建了當前進程的mm_struct、區域結構和頁表的原樣副本。它將兩個進程中的每個頁面都標記爲只讀,並將兩個進程中的每個區域結構都標記爲私有的寫時複製。

當fork在新進程中返回時,新進程現在的虛擬內存剛好和調用fork時存在的虛擬內存相同,當這兩個進程中的任一個後來進行寫操作時,寫時複製機制就會創建新頁面,因此,也就爲每個進程保持了私有地址空間。
在這裏插入圖片描述

7.7 hello進程execve時的內存映射

execve函數在當前進程中加載並運行新程序hello時:

·刪除已存在的用戶區域。刪除當前進程虛擬地址的用戶部分中的已存在的區域結構。

·創建新的區域結構,這些新的區域都是私有的、寫時複製的,代碼和初始化數據映射到.text和.data區,.bss和棧堆映射到匿名文件。

·映射共享區域。如果hello程序與共享對象鏈接,那麼這些對象都是動態鏈接到這個程序的,再映射到用戶虛擬地址空間中的共享區域內。

·設置程序計數器(PC)。設置當前進程上下文中的程序計數器,使之指向代碼區域的入口點。
在這裏插入圖片描述

7.8 缺頁故障與缺頁中斷處理

缺頁中斷:進程線性地址空間裏的頁面不必常駐內存,在執行一條指令時,如果發現他要訪問的頁沒有在內存中(即存在位爲0),那麼停止該指令的執行,併產生一個頁不存在的異常,對應的故障處理程序可通過從外存加載該頁的方法來排除故障,之後,原先引起的異常的指令就可以繼續執行,而不再產生異常。

頁面調度算法:將新頁面調入內存時,如果內存中所有的物理頁都已經分配出去,就按照某種策略來廢棄整個頁面,將其所佔據的物理頁釋放出來。

缺頁中斷的處理:處理函數爲do_page_fault函數,大致流程中爲:

(一)地址爲內核空間:

1,當地址爲內核地址空間並且在內核中訪問時,如果是非連續內存地址,將init_mm中對應的項複製到本進程對應的頁表項做修正;

2,地址爲內核空間時,檢查頁表的訪問權限;

3,如果1,2沒有處理完全,跳到非法訪問處理;

(二)地址爲用戶空間:

4,如果使用了保留位,打印信息,殺死當前進程;

5,如果在中斷上下文中火臨界區中時,直接跳到非法訪問;

6,如果出錯在內核空間中,查看異常表,進行相應的處理;

7,查找地址對應的vma,如果找不到,直接跳到非法訪問處,如果找到正常,跳到good_area;

8,如果vma->start_address>address,可能是棧太小,對齊進行擴展;

9,good_area處,再次檢查權限;

10,權限正確後分配新頁框,頁表等;

7.9動態存儲分配管理

動態內存管理的基本方法與策略:

動態內存分配器維護着一個進程的虛擬虛擬內存區域,稱爲堆(heap)。分配器將堆視爲一組不同大小的塊的集合來維護,每個塊就是一個連續的虛擬內存片,要麼是已分配的,要麼是空閒的。已分配的塊顯式地保留供應用程序使用;空閒塊保持空閒,直到它顯示地被應用所分配。一個已分配的塊保持已分配狀態,直到它被釋放,這種釋放要麼是應用程序顯式執行的,要麼是內存分配器自身隱式執行的。

分配器有兩種風格,兩種風格都要求應用顯式地分配塊,不同之處在於由哪個實體來負責釋放已分配的塊:

(一):顯式分配器

要求應用顯式地釋放任何已分配的塊。顯式分配器的設計需要滿足以下約束條件:處理任意請求序列;立即相應請求;只使用堆;對齊塊;不修改已分配的塊。除此之外,一個好的顯式分配器需達到以下兩個目標:最大化吞吐率和最大化內存利用率。不幸的是,最大化吞吐率和最大化內存利用率二者是相互牽制的,分配器的設計就需要在二者之間找到適當的平衡,通常需要考率以下問題:

  1. 空閒塊組織:如何記錄空閒塊?

  2. 放置:如何選擇一個合適的空閒塊來放置已分配塊?

  3. 分割:在將一個新分配的塊放置到某個空閒塊之後,如何處理這個空閒塊中的剩餘部分?

  4. 合併:如何處理一個剛剛被釋放的塊?

空閒塊組織的三種基本數據結構:

  1. 不帶腳部的隱式空閒鏈表
    在這裏插入圖片描述

  2. 帶腳部的隱式空閒鏈表
    在這裏插入圖片描述

  3. 顯式空閒鏈表
    在這裏插入圖片描述

放置策略:

  1. 首次適配。從頭開始搜索空閒鏈表,選擇第一個合適的塊。

  2. 下一次適配。從上一次查詢結束的地方開始,選擇第一個合適的塊。

  3. 最佳適配。檢測每個空閒塊,選擇適合所需請求大小的最小空閒塊。

合併空閒塊策略:

  1. 立即合併。每次在一個塊釋放時,就合併所有相鄰塊。

  2. 推遲合併。直到某個分配請求失敗,掃描整個堆,合併所有的空閒塊。

(二):隱式分配器

要求分配器檢測一個已分配塊何時不再被程序所使用,那麼就釋放這個塊。隱式分配器也叫垃圾收集器,而自動釋放未使用的已分配塊的過程也叫做垃圾收集。

7.10本章小結

本章簡述了在計算機中的虛擬內存管理,虛擬地址、物理地址、線性地址、邏輯地址的區別以及它們之間的變換模式,重新認識了共享對象、fork和execve,也簡單介紹了動態內存分配的方法與原理。

第8章 hello的IO管理

8.1 Linux的IO設備管理方法

設備的模型化:文件,所有的輸入和輸出都能被當做相應文件的讀和寫來執行。

設備管理:unix io接口,使得所有的輸入和輸出都能以一種統一且一致的方式來執行。

8.2 簡述Unix
IO接口及其函數

Unix IO接口簡述:

1.打開文件。一個應用程序通過要求內核打開相應的文件,來宣告它想要訪問一個I/O設備。內核返回一個小的非負整數,叫做描述符,它在後續對此文件的所有操作中標識這個文件。內核記錄有關這個打開文件的所有信息。應用程序只需記住這個描述符。

2.Linux創建的每個進程開始時都有三個打開的文件:標準輸入(描述符爲0)、標準輸出(描述符爲1)和標準錯誤(描述符爲2)。頭文件<unistd.h>定義了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它們可用來代替顯式的描述符值。

3.改變當前的文件位置。對於每個打開的文件內核保持着一個文件位置k,初始爲0.這個文件位置是從文件開頭起始的字節偏移量。應用程序能夠通過執行seek操作,顯式地設置文件的當前位置爲k

4.讀寫文件。一個讀操作就是從文件複製n>0個字節到內存,從當前文件位置k開始,然後將k增加到k+n。給定一個大小爲m字節的文件,當k>=m時執行讀操作會出發一個稱爲end-of-file(EOF)的條件,應用程序能檢測到這個條件。在文件結尾處沒有明確的“EOF符號”。類似的,寫操作就是從內存複製n>0個字節到一個文件,從當前文件位置k開始,然後更新k。

5.關閉文件。當應用完成了對文件的訪問之後,它就通知內核關閉這個文件。作爲響應,內核釋放文件打開時創建的數據結構,並將這個描述符恢復到可用的描述符池中。無論一個進程因爲何種原因終止時,內核都會關閉所有打開的文件並釋放它們的內存資源。

UnixIO函數:

1.打開文件:int open (char *filename, int
flags, mode_t mode);

2.關閉文件:int close (int fd);

3.讀文件:ssize_t read (int fd, void *buf,
size_t n);

4.寫文件:ssize_t write (int fd, const
void *buf, size_t n);

8.3 printf的實現分析

1.從vsprintf生成顯示信息,到write系統函數,到陷阱-系統調用 int 0x80或syscall.

先看paintf函數的函數體:

int printf(const char *fmt,...)
{
	int i;
	char buf[256];
	va_list arg = (va_list)((char*)(&fmt)+4);
	i=vsprintf(buf,fmt,arg);
	write(buf,i);
	rerurn i;
}

解釋一下參數列表中的“…”:這個是可變形參的一種寫法,當傳遞參數的個數不確定時,就可以用這種方式來表示。

通過va_list arg = (va_list)((char*)(&fmt) + 4)語句找到“…”中的第一個參數。

接着調用了vsprintf函數,該函數返回要打印字符串的長度。

接下來,printf函數會調用系統IO函數:write,其作用就是從緩存buf中最多讀i個字節複製到一個文件位置。

在linux系統中,系統IO被抽象爲文件,包括屏幕。對於系統來說,我們的顯示屏也是一個文件,我們只需要將數據傳送到顯示屏對應的文件,就已經完成了系統端的任務,餘下的工作獨立的由顯示器來進行了。於是在這裏,write會給寄存器傳遞幾個參數,初始化執行環境,然後執行sys call指令,這條指令的作用是產生陷阱異常。陷阱是有意的異常,用戶程序執行了系統調用的命令(syscall)之後,就導致了一個到異常處理程序的陷阱,這個處理程序解析參數,並調用適當的內核程序。

2.字符顯示驅動子程序:從ASCII到字模庫到顯示vram(存儲每一個點的RGB顏色信息)。

3.顯示芯片按照刷新頻率逐行讀取vram,並通過信號線向液晶顯示器傳輸每一個點(RGB分量)。

8.4 getchar的實現分析

函數聲明:int getchar(void)

getchar 有一個int型的返回值。當程序調用getchar時,用戶輸入的字符被存放在鍵盤緩衝區中,直到用戶按回車爲止(回車字符也放在緩 衝區中)。當用戶鍵入回車之後,getchar纔開始從stdin流中每次讀入一個字符。getchar函數的返回值是用戶輸入的第一個字符的ASCII 碼,如出錯返回-1,且將用戶輸入的字符回顯到屏幕。如用戶在按回車之前輸入了不止一個字符,其他字符會保留在鍵盤緩存區中,等待後續getchar調用讀取。也就是說,後續的getchar調用不會等待用戶按鍵,而直接讀取緩衝區中的字符,直到緩衝區中的字符讀完爲後,纔等待用戶按鍵。

進入getchar之後,進程會進入阻塞狀態,等待外界的輸入。系統開始檢測鍵盤的輸入。此時如果按下一個鍵,就會產生一個異步中斷,這個中斷會使系統回到當前的getchar進程,然後根據按下的按鍵,轉化成對應的ascii碼,保存到系統的鍵盤緩衝區。

接下來,getchar調用了read函數。read函數會產生一個陷阱,通過系統調用,將鍵盤緩衝區中存儲的剛剛按下的按鍵信息讀到回車符,然後返回整個字符串。

接下來getchar會對這個字符串進行處理,只取其中第一個字符,將其餘輸入簡單的丟棄,然後將字符作爲返回值,並結束。

8.5本章小結

本章節簡述了Linux系統下I/O的機制,瞭解了有關打開、關閉與讀寫文件的操作,並且分析了printf和getchar兩個函數的實現過程。

結論

hello的一生會經歷如下階段:(1) 預處理:預處理器cpp將.c文件翻譯成.i的文件;(2) 編譯:gcc編譯器將.i文件翻譯成.s格式的彙編語言文件;(3) 彙編:as彙編器將.s文件轉換成十六進制機器碼的.o文件;(4) 鏈接:ld鏈接器將一系列.o文件鏈接起來形成最終的可執行文件hello;(5) 進程創建:shell爲hello程序fork一個子進程;(6) 程序運行:shell調用execve函數,映射虛擬內存,載入物理內存,進入main函數;(7) 指令執行:hello和其他進程併發地運行,CPU爲其分配時間片;(8) 進程回收:shell回收子進程,系統釋放該進程的數據所佔的內存空間。

附件

hello.c:hello源程序

hello.i:hello.c預處理產生的ASCII文件

hello.s:hello.i編譯產生的彙編代碼文件

hello.o:彙編後產生的可重定位文件

hello:可重定位目標文件鏈接後的可執行文件

參考文獻

[1] https://blog.csdn.net/ylcangel/article/details/18188921

[2] https://blog.csdn.net/i_am_jm/article/details/90721973

[3] https://zhuanlan.zhihu.com/p/39354226.

[4]https://blog.csdn.net/zheng123123123123/article/details/13017655

[5] https://blog.csdn.net/fallingu/article/details/75221276

[6] https://blog.csdn.net/balian8/article/details/78831626

[7]https://blog.csdn.net/m0_37962600/article/details/81448553

[8]https://blog.csdn.net/hulifangjiayou/article/details/40480467

[9] 深入理解計算機系統第三版

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章