《操作系统真象还原》-阅读笔记(下)
第十一章
任意進程的頁目錄表第0~767個頁目錄項屬于用戶空間,指向用戶頁表。第768~1023個頁目錄項指向內(nèi)核頁表。每創(chuàng)建一個新的用戶進程,就將內(nèi)核頁目錄項復(fù)制到用戶進程的頁目錄表,其次需要把用戶頁目錄表中最后一個頁目錄項更新為用戶進程自己的頁目錄表的物理地址。
每個進程有自己單獨的位圖,存儲在進程pcb中的userprog_vaddr中。
LDT
當(dāng)前運行的任務(wù),其LDT位于LDTR指向的地址(選擇子)。每切換一個任務(wù)時,需要lldt指令重新加載新任務(wù)的LDT到LDTR
TSS
CPU自動用此結(jié)構(gòu)體變量保存任務(wù)的狀態(tài)(任務(wù)的上下文環(huán)境,寄存器的值)和自動從此結(jié)構(gòu)體變量中載入任務(wù)的狀態(tài)。TR寄存器始終指向當(dāng)前任務(wù)的TSS,這樣每個任務(wù)必須有單獨的TSS,但Linux并未這么做。Linux所有任務(wù)共享一個TSS。
除了中斷和調(diào)用門返回外,CPU不允許從高特權(quán)級到低特權(quán)級。CPU在不同的特權(quán)級下使用不同的棧。
TSS和LDT一樣,必須要在GDT中注冊。
現(xiàn)代操作系統(tǒng)采用的任務(wù)切換方式
我們使用TSS的唯一理由是為0特權(quán)級的任務(wù)提供棧。
Linux中為每個CPU創(chuàng)建一個TSS,在各個CPU上的所有任務(wù)共享同一個TSS,各CPU的TR寄存器保存各自的TSS,在用ltr指令加載TSS后,TR寄存器永遠指向同一個TSS,進程切換時只把這個TSS中的SS0和esp0更新為新任務(wù)的內(nèi)核棧的段地址和棧指針。
第十二章
系統(tǒng)調(diào)用
Linux只占用一個中斷向量號0X80,在寄存器eax中寫入子功能號。所有系統(tǒng)調(diào)用都可以通過syscall函數(shù)(不是由系統(tǒng)提供的,是glibc提供的庫函數(shù),直接系統(tǒng)調(diào)用為_syscall)完成。總之對用戶進程而言,在 Linux 上執(zhí)行系統(tǒng)調(diào)用,只需要提供子功能號和參數(shù)就行了。
堆內(nèi)存管理
arena
將一大塊內(nèi)存劃分為無數(shù)小內(nèi)存塊的內(nèi)存?zhèn)}庫。
arena 是個提供內(nèi)在分配的數(shù)據(jù)結(jié)構(gòu),它分為兩部分,一部分是元信息,用來描述自己內(nèi)存池中空閑內(nèi)存塊數(shù)量,這其中包括內(nèi)存塊描述符指針(后面介紹),通過它可以間接獲知本 arena 所包含內(nèi)存塊的規(guī)格大小,此部分占用的空間是固定的,約為 12 字節(jié)。另一部分就是內(nèi)存地區(qū)域,這里面有無數(shù)的內(nèi)存塊,此部分占用 arena 大量的空間。
本書中針對小內(nèi)存塊的arena占用1頁框內(nèi)存。
每個內(nèi)存塊命名為mem_block,分別為每一種規(guī)格的內(nèi)存塊建立一個內(nèi)存塊描述符即mem_bloc_desc。
struct mem_block_desc (
uint32 t block size //內(nèi)存塊大小
uint32_t blocks_per_arena; //本arena中可容納此 mem_block 的數(shù)量
struct list free list //目前可用同類的mem_block鏈表
實現(xiàn)sys_malloc
傳入?yún)?shù)size,代表申請多少字節(jié)內(nèi)存
1.首先判斷用哪個內(nèi)存池,內(nèi)核還是用戶
2.若申請的內(nèi)存不在內(nèi)存池容量范圍內(nèi),直接返回NULL
3.超過1024就直接分配頁框
4.若小于等于1024,循環(huán)各種規(guī)格,找到最合適的規(guī)格
5.判斷該規(guī)格free_list是否為空,若mem_block_desc的free_list中已經(jīng)沒有可用的mem_block,就創(chuàng)建新的arena提供mem_block
6.從free_list中彈出一個內(nèi)存塊,通過elem2entry宏轉(zhuǎn)換成mem_block的地址,返回內(nèi)存塊地址
內(nèi)存釋放(頁框級別)
分配內(nèi)存的步驟
1.虛擬地址池中分配虛擬地址,操作位圖
2.物理內(nèi)存池中分配物理地址,操作位圖
3.完成虛擬地址和物理地址的映射
釋放內(nèi)存的步驟
1.釋放物理頁地址,操作位圖
2.在頁表中去掉虛擬地址的映射,將虛擬地址對應(yīng)pte的P位置0
3.在虛擬地址中釋放虛擬地址,操作位圖
當(dāng)物理內(nèi)存不多時,就將其數(shù)據(jù)移到硬盤中,然后對應(yīng)頁表項pte的P位置0,當(dāng)CPU訪問時會引發(fā)pagefault中斷,中斷處理程序?qū)?shù)據(jù)物理頁更新到pte中,再將P位置1,CPU會再次訪問引起pagefault的虛擬地址。
實現(xiàn)sys_free
對于大內(nèi)存,就是把頁框在虛擬內(nèi)存池和物理內(nèi)存池的位圖中相應(yīng)位置0。
對于小內(nèi)存,是將arena中的內(nèi)存塊重新放回到內(nèi)存塊描述符的空閑塊鏈表free_list。
第十三章
編寫硬盤驅(qū)動(略)
第十四章
硬盤的讀寫單位是扇區(qū),數(shù)據(jù)一般積攢到足夠大小才一次性訪問硬盤,足夠大小的數(shù)據(jù)就是塊,一個塊由多個扇區(qū)構(gòu)成。
FAT32
用鏈表的方式來連接每個數(shù)據(jù)塊,查詢某個數(shù)據(jù)塊很耗時
inode
控制,管理文件相關(guān)信息的數(shù)據(jù)結(jié)構(gòu)是FCB,inode就是其中一種。
用索引來查找數(shù)據(jù)塊,在UNIX系統(tǒng)中,一個文件必須對應(yīng)一個inode(索引表),磁盤中有多少文件就有多少個inode。inode的結(jié)構(gòu)如下如圖,前12個索引為直接指針,后面的3個為間接索引的地址。每個間接索引表都可存256個塊。
在Linux中每分區(qū)inode數(shù)量是固定的,分區(qū)中所有文件的inode通過一個大表格來維護,此表格稱為inode_table。
目錄項
通過文件名找文件實體數(shù)據(jù)的流程是:
1.在目錄中找到文件名所在的目錄項
2.從目錄項中獲取inode編號
3.用inode編號作為inode數(shù)組的索引下標(biāo),找到inode
4.從該inode中獲取數(shù)據(jù)塊的地址,讀取數(shù)據(jù)塊
目錄項僅存在于inode指向的數(shù)據(jù)塊中,有目錄項的數(shù)據(jù)塊就是目錄,目錄項所屬的inode指向的所有數(shù)據(jù)塊便是目錄。
每個分區(qū)都有自己的根目錄,根目錄/的位置是固定不變的,查找任意文件時,都直接到根目錄的數(shù)據(jù)塊中找相關(guān)的目錄項,然后遞歸查找,最終可以找到任意子目錄中的文件。
超級塊
超級塊是保存文件系統(tǒng)元信息的元信息。它被固定在各分區(qū)的第2個扇區(qū)。
文件系統(tǒng)布局
Linux早期文件系統(tǒng)布局如下圖
第十五章
fork
fork()會產(chǎn)生一個和父進程完全相同的子進程,但子進程在此后多會exec系統(tǒng)調(diào)用,出于效率考慮,Linux中引入了“寫時復(fù)制“技術(shù),也就是只有進程空間的各段的內(nèi)容要發(fā)生變化時,才會將父進程的內(nèi)容復(fù)制一份給子進程。在fork之后exec之前兩個進程用的是相同的物理空間(內(nèi)存區(qū)),子進程的代碼段、數(shù)據(jù)段、堆棧都是指向父進程的物理空間,也就是說,兩者的虛擬空間不同,但其對應(yīng)的物理空間是同一個。當(dāng)父子進程中有更改相應(yīng)段的行為發(fā)生時,再為子進程相應(yīng)的段分配物理空間,如果不是因為exec,內(nèi)核會給子進程的數(shù)據(jù)段、堆棧段分配相應(yīng)的物理空間(至此兩者有各自的進程空間,互不影響),而代碼段繼續(xù)共享父進程的物理空間(兩者的代碼完全相同)。而如果是因為exec,由于兩者執(zhí)行的代碼不同,子進程的代碼段也會分配單獨的物理空間。
為什么fork后父子進程同一個變量地址打印出來相同
假定父進程malloc的指針指向0x12345678, fork 后,子進程中的指針也是指向0x12345678,但是這兩個地址都是虛擬內(nèi)存地址 (virtual memory),經(jīng)過內(nèi)存地址轉(zhuǎn)換后所對應(yīng)的 物理地址是不一樣的。所以兩個進城中的這兩個地址相互之間沒有任何關(guān)系。
fork時子進程獲得父進程數(shù)據(jù)空間、堆和棧的復(fù)制,所以變量的地址(當(dāng)然是虛擬地址)也是一樣的。
每個進程都有自己的虛擬地址空間,不同進程的相同的虛擬地址顯然可以對應(yīng)不同的物理地址。因此地址相同(虛擬地址)而值不同沒什么奇怪。
具體過程是這樣的:
fork子進程完全復(fù)制父進程的棧空間,也復(fù)制了頁表,但沒有復(fù)制物理頁面,所以這時虛擬地址相同,物理地址也相同,但是會把父子共享的頁面標(biāo)記為“只讀”(類似mmap的private的方式),如果父子進程一直對這個頁面是同一個頁面,知道其中任何一個進程要對共享的頁面“寫操作”,這時內(nèi)核會復(fù)制一個物理頁面給這個進程使用,同時修改頁表。而把原來的只讀頁面標(biāo)記為“可寫”,留給另外一個進程使用。
這就是所謂的“寫時復(fù)制”。正因為fork采用了這種寫時復(fù)制的機制,所以fork出來子進程之后,父子進程哪個先調(diào)度呢?內(nèi)核一般會先調(diào)度子進程,因為很多情況下子進程是要馬上執(zhí)行exec,會清空棧、堆。。這些和父進程共享的空間,加載新的代碼段。。。,這就避免了“寫時復(fù)制”拷貝共享頁面的機會。如果父進程先調(diào)度很可能寫共享頁面,會產(chǎn)生“寫時復(fù)制”的無用功。所以,一般是子進程先調(diào)度滴。
(注1:在理解時,你可以認為fork后,這兩個相同的虛擬地址指向的是不同的物理地址,這樣方便理解父子進程之間的獨立性)
(注2:但實際上,Linux為了提高 fork 的效率,采用了 copy-on-write 技術(shù),fork后,這兩個虛擬地址實際上指向相同的物理地址(內(nèi)存頁),只有任何一個進程試圖修改這個虛擬地址里的內(nèi)容前,兩個虛擬地址才會指向不同的物理地址(新的物理地址的內(nèi)容從原物理地址中復(fù)制得到))
wait和exit
進程間通信必須要借助內(nèi)核(無論管道、消息隊列、還是共享內(nèi)存等進程間通信形式),子進程的返回值肯定是先交給內(nèi)核,然后父進程向內(nèi)核要子進程的返回值。父進程調(diào)用pid_t wait(int *status)后,內(nèi)核就把子進程的返回值存儲到status指向的內(nèi)存空間。子進程的返回值存放在它的PCB中,調(diào)用exit后內(nèi)核會把進程占用的大部分資源回收,比如內(nèi)存、頁表等,但不能回收PCB,需要將其中的返回值交給父進程后才能回收。
exit調(diào)用表面上是結(jié)束子進程運行并傳遞返回值給內(nèi)核,本質(zhì)上是內(nèi)核在幕后將進程除了PCB以外的所有資源回收。
孤兒進程
父進程提前退出,父進程的子進程會稱為孤兒進程,被init進程收養(yǎng)
僵尸進程
僵尸進程就是沒有父進程來給某進程收尸,也就是調(diào)用wait收取返回值,因此其PCB一直不能被回收。
管道
Linux中管道實現(xiàn)如下圖:
其實就是對一頁框大小的內(nèi)存區(qū)域做讀寫操作
匿名管道:在內(nèi)核中,父子進程因為有相同的管道描述符,所以都可以訪問這個管道。
有名管道:在文件系統(tǒng)中創(chuàng)建一個管道文件(FIFO,是一種特殊的文件類型),使得該管道對任何進程都可見,因此沒有父子關(guān)系也能通信。當(dāng)一個進程以讀(r)的方式打開該文件,而另一個進程以寫(w)的方式打開該文件,那么內(nèi)核就會在這兩個進程之間建立管道,所以FIFO實際上也由內(nèi)核管理,不與硬盤打交道。
Linux共享內(nèi)存,信號
http://blog.csdn.net/lqygame/article/details/71424917
http://blog.csdn.net/lqygame/article/details/73555430
寫在最后
每天晚上看一點,歷時接近1個月終于把這本書讀完,感謝鋼哥,這本書刷新了我對操作系統(tǒng)的認知,總算對操作系統(tǒng)有了個很全面的了解。這些筆記是針對我自己的一些感覺需要記錄下來的東西,OS還需要學(xué)習(xí)的東西還有很多啊。
--------------------- ?
作者:月黑風(fēng)高云游詩人 ?
來源:CSDN ?
原文:https://blog.csdn.net/lqygame/article/details/73850249 ?
版權(quán)聲明:本文為博主原創(chuàng)文章,轉(zhuǎn)載請附上博文鏈接!
總結(jié)
以上是生活随笔為你收集整理的《操作系统真象还原》-阅读笔记(下)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 国内第四大运营商!山东广电192友好预约
- 下一篇: 【转】C#执行rar,zip文件压缩的几