Linux從頭學(xué):操作系統(tǒng)-如何把頁(yè)目錄和頁(yè)表當(dāng)做普通物理頁(yè)進(jìn)行操作的?
目錄
問(wèn)題描述
- CPU接收的是線性地址,不是物理地址
對(duì)頁(yè)目錄進(jìn)行"自操作"
- 一級(jí)查表:構(gòu)造線性地址的前十位
- 二級(jí)查表:構(gòu)造線性地址的中間十位
- 三級(jí)查表:構(gòu)造線性地址的最后十二位
- 三個(gè)地址段合體
對(duì)頁(yè)表進(jìn)行"自操作"
在 x86 系統(tǒng)中,內(nèi)存管理中的分頁(yè)機(jī)制是非常重要的,在Linux操作系統(tǒng)相關(guān)的各種書(shū)籍中,這部分內(nèi)容也是重筆濃彩。
如果你看過(guò) Linux 內(nèi)核相關(guān)書(shū)籍,一定對(duì)下面這張圖又熟悉、又恐懼:

這是 Linux 系統(tǒng)中,頁(yè)處理單元的多級(jí)頁(yè)表查詢方式。
其中黃色背景部分:頁(yè)上級(jí)目錄索引 和 頁(yè)中間目錄索引,是 Linux 系統(tǒng)自己擴(kuò)展的,在原本的 x86 處理器中是不存在的,這也是導(dǎo)致 Linux 中相關(guān)部分代碼更加復(fù)雜的原因。
在上一篇文章中,我們主要對(duì) x86 中的頁(yè)目錄和頁(yè)表的“反向構(gòu)造”、“正向查找”這兩個(gè)過(guò)程進(jìn)行了圖文并茂的討論。文章鏈接在此:Linux從頭學(xué)15:【頁(yè)目錄和頁(yè)表】-理論 + 實(shí)例 + 圖文的最完全、最接地氣詳解!,但是其中有一個(gè)環(huán)節(jié)被特意忽略過(guò)去了。
那就是:在操作系統(tǒng)構(gòu)造頁(yè)目錄和頁(yè)表的時(shí)候,如何對(duì)它們自身進(jìn)行尋址和操作?
這部分內(nèi)容,也是內(nèi)存管理中比較復(fù)雜的地方,就好比一名醫(yī)生給病人做手術(shù),但是病人卻是“醫(yī)生自己”。
這篇文章,我們繼續(xù)通過(guò)圖片+實(shí)例的方式,一起來(lái)研究一下內(nèi)核代碼一般都是如何來(lái)進(jìn)行這些“自操作”的。
把這里面的操作機(jī)制研究透徹之后,再去看 Linux 內(nèi)核代碼時(shí),就不會(huì)暈頭轉(zhuǎn)向了。
問(wèn)題描述
在上一篇文章中,我們舉了這樣一個(gè)示例:
- 假設(shè)實(shí)際的物理內(nèi)存是1 GB;
- 用戶程序文件在硬盤上的長(zhǎng)度是20 MB;
- 操作系統(tǒng)把用戶程序加載到內(nèi)存中時(shí),從 0x4000_0000 的虛擬內(nèi)存地址處開(kāi)始存放;
- 操作系統(tǒng)讀取程序結(jié)束后,為所有的地址構(gòu)造好了頁(yè)目錄和頁(yè)表;
如下圖所示:

頁(yè)目錄和頁(yè)表的每一個(gè)有效表項(xiàng)中,存儲(chǔ)的地址都是一個(gè)個(gè)實(shí)實(shí)在在的物理頁(yè)的前 20 位(因?yàn)橐粋€(gè)物理頁(yè)的長(zhǎng)度固定是 4KB,在分配時(shí)都是對(duì)齊的,末尾的 12 位全部為 0)。
并且頁(yè)目錄和頁(yè)表“們”自身,都占用一個(gè)物理頁(yè)的空間,所以它們都有自己的物理地址。
當(dāng)頁(yè)目錄和頁(yè)表都構(gòu)造妥當(dāng)之后,處理器面對(duì)一個(gè)線性地址,例如:0x4100_1800,頁(yè)處理單元就會(huì)按照分級(jí)查表的方式,把這個(gè)線性地址轉(zhuǎn)換為一個(gè)物理地址:
- 拆分線性地址:0x4100_1800 = 0100_0001_0000_0000___0001_1000_0000_0000;
- 根據(jù)線性地址的前 10 位,找到頁(yè)目錄中的索引 260,從而確定頁(yè)表的物理地址是 0x0800_4000(表項(xiàng)中的值是 0x08004,還要補(bǔ)上低位的 12 個(gè) 0);
- 根據(jù)線性地址的中間 10 位,找到 0x0800_4000 這個(gè)頁(yè)表中的索引 1,從而確定普通物理頁(yè)的物理地址是 0x0210_1000(表項(xiàng)中的值是 0x02101,還要補(bǔ)上低位的 12 個(gè) 0);
- 根據(jù)線性地址的最后 12 位,確定普通頁(yè)內(nèi)的偏移量是 2048,普通頁(yè)的開(kāi)始地址加上這個(gè)偏移量,就得到了最終的物理地址 0x0210_1800。
詳細(xì)的討論過(guò)程,請(qǐng)參考上一篇文章:Linux從頭學(xué)15:【頁(yè)目錄和頁(yè)表】-理論 + 實(shí)例 + 圖文的最完全、最接地氣詳解!。
那么,問(wèn)題來(lái)了:
在頁(yè)處理單元開(kāi)啟的情況下,處理器面對(duì)的是線性地址,那么操作系統(tǒng)在構(gòu)造頁(yè)目錄中的每一個(gè)表項(xiàng)的時(shí)候,如何對(duì)這個(gè)表項(xiàng)進(jìn)行尋址?
具體到上圖來(lái)說(shuō)就是:操作系統(tǒng)想把第一個(gè)頁(yè)表的物理地址 0x0800_0000,填寫到頁(yè)目錄的第 256 個(gè)表項(xiàng)中時(shí),那么 CPU 就需要找到這個(gè)表項(xiàng),這個(gè)表項(xiàng)肯定有物理地址的。
但是,我們不能把這個(gè)表項(xiàng)的物理地址直接告訴 CPU,因?yàn)?CPU 只接收線性地址,它會(huì)自動(dòng)經(jīng)過(guò)分頁(yè)單元的處理來(lái)得到對(duì)應(yīng)的物理地址。
那么,這個(gè)線性地址的值應(yīng)該是多少呢?
繼續(xù)用實(shí)例來(lái)說(shuō)明,這樣容易理解。
假設(shè)頁(yè)目錄所處的物理頁(yè)開(kāi)始地址是 0x0100_0000,那么第256個(gè)表項(xiàng)的物理地址就是 0x0100_0400。

有些小伙伴可能會(huì)說(shuō):直接把物理地址 0x0100_0400 告訴處理器,不就可以了嗎?
這是不對(duì)的!
處理器接收的是線性地址,不是物理地址
因?yàn)楝F(xiàn)在已經(jīng)開(kāi)啟了分頁(yè)處理單元,0x0100_0400 是我們最后想得到的物理地址,而處理器只接受線性地址,雖然我們知道這是一個(gè)物理地址,但是處理器不知道啊!
當(dāng)我們給處理器一個(gè)地址的時(shí)候,處理器會(huì)按部就班的對(duì)這個(gè)地址進(jìn)行[段轉(zhuǎn)換],再進(jìn)行[頁(yè)轉(zhuǎn)換],這時(shí)才得到它認(rèn)為的物理地址。
由于使用的是“平坦型”的段結(jié)構(gòu),所以這里就忽略了段處理過(guò)程,直接討論頁(yè)處理過(guò)程。
所以,我們應(yīng)該使用某些方法,構(gòu)造出一個(gè)線性地址 addr,讓這個(gè)地址經(jīng)過(guò)頁(yè)處理單元之后,得到 0x0100_0400 這個(gè)物理地址:
這里有點(diǎn)遞歸的味道,又有點(diǎn)像一個(gè)醫(yī)生給他自己做一個(gè)外科手術(shù)!
現(xiàn)在,應(yīng)該明白面對(duì)的問(wèn)題了吧?
目標(biāo)就是:通過(guò)某種方法,構(gòu)造出一個(gè)線性地址 addr,并且通過(guò)頁(yè)處理單元轉(zhuǎn)換之后,得到物理地址 0x0100_0400。
對(duì)頁(yè)目錄進(jìn)行操作
重新梳理一下思路:如果對(duì)一個(gè)普通物理頁(yè)(下文簡(jiǎn)稱為:普通頁(yè))里的一個(gè)地址處的數(shù)據(jù)進(jìn)行操作,需要經(jīng)過(guò)3次查表操作:
從頁(yè)表的某個(gè)表項(xiàng)中,找到的那個(gè)物理地址,就是最后要操作的普通物理頁(yè)。
現(xiàn)在我們的問(wèn)題是:需要把頁(yè)目錄作為最終的操作對(duì)象。
也就是說(shuō),從頁(yè)表中找到的“普通頁(yè)”的物理地址,應(yīng)該等于頁(yè)目錄的物理地址!
作為一名軟件開(kāi)發(fā)人員,遞歸思想都是有的。
我們就來(lái)構(gòu)造一個(gè)線性地址 addr,讓它經(jīng)過(guò)3次查表操作之后,能夠指向頁(yè)目錄的物理地址。
一級(jí)查表:構(gòu)造線性地址的前 10 位,來(lái)確定頁(yè)表的物理地址
一級(jí)查表:查找的對(duì)象是頁(yè)目錄。
線性地址addr的前10位,決定了頁(yè)目錄內(nèi)的索引。
很顯然,需要讓這個(gè)索引對(duì)應(yīng)的那個(gè)表項(xiàng)中所登記的地址,必須是指向頁(yè)目錄自己才可以。
常用的解決方案是:利用頁(yè)目錄中的最后一個(gè)表項(xiàng),讓這個(gè)表項(xiàng)中記錄的地址,指向頁(yè)目錄自己,如下圖所示:
也就是說(shuō),預(yù)先在頁(yè)目錄的最后一個(gè)表項(xiàng)中,填入頁(yè)目錄自己的物理地址,然后只要線性地址addr前10位的值為 1023,就能夠得到這個(gè)表項(xiàng)。
很容易就能得到addr的前10位應(yīng)該是:0x3FF(二進(jìn)制:1111_1111_11)。
由于這個(gè)表項(xiàng)中存儲(chǔ)的地址是頁(yè)目錄自己的開(kāi)始地址(0x0100_0000, 最后的12個(gè)0是自動(dòng)補(bǔ)上的),這樣就相當(dāng)于:下面進(jìn)入第二級(jí)查找時(shí),頁(yè)目錄即將被當(dāng)做“頁(yè)表”來(lái)使用。
如下圖所示:

這里紅色虛線的“頁(yè)表”其實(shí)就是頁(yè)目錄自己,只是一個(gè)影子而已。
二級(jí)查表:構(gòu)造線性地址的中間 10 位,來(lái)確定“普通頁(yè)”的物理地址
二級(jí)查表:查找的對(duì)象是頁(yè)表,也就是一級(jí)查表得到的那個(gè)“頁(yè)表”。
雖然一級(jí)查表的結(jié)果是頁(yè)目錄自己,但是處理器不管這些,它會(huì)把這個(gè)表當(dāng)做頁(yè)表來(lái)使用。
現(xiàn)在,來(lái)考慮線性地址addr的中間10位,它決定了頁(yè)表中的索引號(hào)。
很顯然,需要繼續(xù)讓這個(gè)索引號(hào)對(duì)應(yīng)的那個(gè)表項(xiàng)中,記錄的地址必須繼續(xù)指向頁(yè)目錄自己。
那就繼續(xù)利用這個(gè)“頁(yè)表”(其實(shí)它是頁(yè)目錄)中的最后一個(gè)表項(xiàng)唄,就是index = 1023的這個(gè)表項(xiàng)。
這個(gè)表項(xiàng)中存儲(chǔ)的物理地址,即將是最終查表得到的“普通頁(yè)”的物理地址了。
由于這個(gè)表項(xiàng)中,被預(yù)先填寫了 0x01000,補(bǔ)上尾部的12個(gè)0之后就是 0x0100_0000,仍然指向頁(yè)目錄自己,完美!
于是,就得到了中間10位的結(jié)果:0x3FF(二進(jìn)制:11_1111_1111)。
如下圖所示:
最右面紅色虛線的“物理頁(yè)”,就是二級(jí)查找的結(jié)果,它本質(zhì)上仍然是頁(yè)目錄本身,只不過(guò)它即將被當(dāng)做一個(gè)普通物理頁(yè)來(lái)使用。
三級(jí)查表:構(gòu)造線性地址的最后 12 位,來(lái)確定“普通頁(yè)”的頁(yè)內(nèi)偏移量
現(xiàn)在,已經(jīng)構(gòu)造出了線性地址addr(這是我們的最終目標(biāo))的前20位,并且經(jīng)過(guò)頁(yè)表的前兩級(jí)查表,成功的定位到了頁(yè)目錄自己!
就差最后一步了!
我們知道,從線性地址到物理地址的轉(zhuǎn)換過(guò)程中,最后的12位表示頁(yè)內(nèi)偏移,是直接從線性地址中取過(guò)來(lái)的。
也就是說(shuō):線性地址 與 物理地址 的最后12位偏移量,值是一樣的!
所以,我們就反過(guò)來(lái)倒推一下:
我們最終想操作的是頁(yè)目錄中第256個(gè)表項(xiàng),它的物理地址是 0x0100_0400,這個(gè)物理地址距離這個(gè)頁(yè)目錄開(kāi)始位地址的偏移量是:0x400(0x0100_0400 減去 0x0100_0000)。
因此,線性地址addr中的最后12位的值也應(yīng)該是 0x400。
三個(gè)地址段合體
把上面三個(gè)步驟中,得到的地址聚合在一起:
0xFFFF_F400 就是最終想得到的線性地址!
也就是說(shuō),我們只要把這個(gè)線性地址 0xFFFF_F400 告訴處理器,它就會(huì)經(jīng)過(guò)頁(yè)處理單元的轉(zhuǎn)換,最終查找到頁(yè)目錄這個(gè)物理頁(yè)中的第 256個(gè)表項(xiàng),也就是物理地址 0x0100_0400。
例如:mov [0xFFFF_4000], xxxx
以上就是操作系統(tǒng)在操作頁(yè)目錄自身時(shí),所采取的策略。
具體到每個(gè)操作系統(tǒng)來(lái)說(shuō),可能稍微有差別,但是其中的道理都是差不多的。
例如本文開(kāi)頭的第一張圖中,Linux 使用了4級(jí)表格來(lái)查找,并且中間的兩個(gè)表格還可以省略不用。
如何跨過(guò)中間的這兩個(gè)表格,Linux 內(nèi)核代碼中的代碼更復(fù)雜一些,但是策略都是一樣的。
對(duì)頁(yè)表進(jìn)行尋址
既然已經(jīng)弄明白了操作系統(tǒng)是如何操作頁(yè)目錄的,那么對(duì)頁(yè)表的操作就不是什么大問(wèn)題了。
比如下面這張圖:
目標(biāo):把最右面的普通物理頁(yè)地址 0x0200_0000,放入 0x0800_0000 這個(gè)頁(yè)表的第一個(gè)表項(xiàng)中(只需要存儲(chǔ)前20位),那么應(yīng)該傳遞什么樣的線性地址給處理器?
思路是完全一樣的。
一級(jí)查表
按照正常的分頁(yè)查找流程,從頁(yè)目錄的某個(gè)表項(xiàng)中,查找我們想操作的那個(gè)頁(yè)表。
頁(yè)目錄中的這個(gè)表項(xiàng)位于索引值256的地方,因此可以構(gòu)造出線性地址的前10位是:0100_0000_00(0x100)。
所以,經(jīng)過(guò)一級(jí)查表得到的這個(gè)頁(yè)表的物理地址是 0x0800_0000。
二級(jí)查表
利用這個(gè)頁(yè)表的最后一個(gè)表項(xiàng)(index = 1023),預(yù)先填寫一個(gè)地址(0x08000),讓它指向這個(gè)頁(yè)表自己的開(kāi)始物理地址。
于是,可以構(gòu)造出線性地址的中間10位是:11_1111_1111(0x3FF)。
由于這個(gè)表項(xiàng)中存儲(chǔ)的地址是 0x0800_0000,指向的正是頁(yè)表自己,只不過(guò)馬上它就被當(dāng)作普通物理頁(yè)被使用。
三級(jí)查表
此時(shí),已經(jīng)找到最后的普通物理頁(yè)了(其實(shí)它是一個(gè)頁(yè)表,被當(dāng)作普通物理頁(yè)使用)。
線性地址的最后12位,可以直接從最后想操作的那個(gè)目標(biāo)物理地址中最后12位直接拿過(guò)來(lái)。
我們的目標(biāo)是:操作頁(yè)表中的第 0 個(gè)表項(xiàng),這個(gè)表項(xiàng)的物理地址是 0x0800_0000,最后的12位偏移量是 0000_0000_0000。
把以上3個(gè)地址段合體,即可得到正確的線性地址:
這里討論的方法,并不是處理頁(yè)目錄和頁(yè)表的唯一方式。
當(dāng)處理邏輯更加復(fù)雜時(shí),可能需要對(duì)頁(yè)目錄或頁(yè)表中更多的表項(xiàng),進(jìn)行一些特殊的預(yù)處理。
如果你想挑戰(zhàn)一下,可以看一下Linux內(nèi)核中的相關(guān)文檔或代碼!
在這個(gè)系列中,關(guān)于頁(yè)目錄和頁(yè)表的知識(shí)點(diǎn)就介紹結(jié)束了。
如果文中有錯(cuò)誤或者誤導(dǎo)的地方,非常期待與您一起探討、學(xué)習(xí)!
寫這篇文章真不容易,讓我深深的體會(huì)到那句話:
寫作就是:將網(wǎng)狀的思考-通過(guò)樹(shù)狀的結(jié)構(gòu)-用線性的語(yǔ)言清晰的表達(dá)出來(lái)。
本文轉(zhuǎn)載自微信公眾號(hào)「IOT物聯(lián)網(wǎng)小鎮(zhèn)」
【編輯推薦】