Linux核心原始碼分析--記憶體管理(二、函式實現技巧)
仔細的分析了一下各個記憶體管理函式的實現,發現裡面涉及到了幾個技巧,如果知道了這幾個技巧,那麼閱讀記憶體管理原始碼將會事半功倍(主要是這幾個技巧在幾個函式中都出現過),當然也會選擇性的分析幾個比較重要的函式實現;
函式實現技巧
1、向上取整:以一個頁面為了例,如果地址是1,那麼向上取整就是4096;如果地址是 4095,向上取整就是4096;如果地址是4098,向上取整就是4096 x 2.......
這個是比較addr地址是否在程序的程式碼段內,至於為什麼要這樣實現,到現在我還沒弄清楚(等看完程序排程那塊再到回來看看);這裡用到一個技巧就是向上取整,addr是一個隨機地址,要找出該地址的下一個頁面的物理起始地址,那麼就要向上取整了。(addr + 4095) & (~4095) :addr如果是4096的倍數,那麼(addr+4095) & (~4095)結果就是addr;如果addr不是4096的倍數,那麼addr以4096取餘後得到的值範圍為:1~4095,再加上4095得到範圍:4096~4096+4094;然後&(~4095)就可以得到一個完整的4096了;其實上面的 &(~4095)就是相當於整除4096:/4096;可以使用(x + 9)/ 10,(其中x為任意數)來驗證下;#define CODE_SPACE(addr) ((((addr)+4095)&~4095) < \ current->start_code + current->end_code)
在free_page_tables()函式中:size = (size + 0x3fffff) >> 22;也是向上取整的例項;
2、獲取目錄項/表項的物理起始地址:一般是從線性地址中獲取到目錄項/表項號,然後右移2個位元組(因為一個項佔用4個位元組)就可以得到物理起始地址,也稱目錄項指標;
dir = (unsigned long *) ((from>>20) & 0xffc);
上面函式在free_page_tables()中(其他函式中有),from是線性地址,要得到目錄項號,則:from = from >> 22(線性地址中和目錄項有關的只有高10,這裡就是獲取到高10位的地址);注意這裡獲取到的僅僅只是目錄項的號,而不是目錄項的實體地址。
又根據一個目錄項佔用4個位元組,那麼from = from << 2(左移2位表示 乘以 2^2);所以兩個合起來就乾脆只右移20位得了,那麼就有上面的 from >> 20了。但是這樣有個問題:開始資料為1111,右移2位結果:1111 >> 2 為 0011;接著左移回2位結果:0011 << 2 為 1100,而開始的結果為1111,所以若按照這個來移動的話就會出錯的。但是如果後面兩位為0,則結果是正確的,相當於少移的2位是空的。
為了解決上面的問題,乾脆把低2位和諧掉,因為如果11,12位是0,那倒無所謂;但如果不是0,那麼結果就出錯了。0xffc == 1111 1111 1100 ,或上它就是把低2位幹掉(用這種方法可以驗證下 1111 移動問題)。其實本來低2位就是要捨棄的,因為目錄項是4位元組對齊的;
同理,在其他函式中要獲取頁表項的實體地址方法類似:((address>>10) & 0xffc) 在write_verify()函式中有實現;
3、修改陣列對映值:這個應該不是函式設計技巧,但在記憶體管理中卻頻繁出現,而且很重要。
this_page -= LOW_MEM; // 地址減去1MB表示主記憶體中的相對地址
this_page >>= 12; // 右移12位表示以頁為單位的物理起始頁
mem_map[this_page]++;// 對相應的物理頁修改對映值
其是上面的方法在最開始的地方用巨集也實現過:#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)//指定記憶體地址對映為頁號;
函式列表簡介
1、static inline volatile void oom(void);內斂函式,顯示記憶體已經用完,返回碼11(資源不可用);2、#define invalidate() __asm__("movl %%eax,%%cr3"::"a" (0)) 重新整理頁快取記憶體區,其實就是重新載入下頁目錄起始地址,0 --->> CR3;
3、#define copy_page(from,to) __asm__("cld ; rep ; movsl"::"S" (from),"D" (to),"c" (1024):"cx","di","si") 從from拷貝一頁的內容到to,其中我發現一個問題:為什麼在核心的嵌入彙編中都不指定段描述符?向這裡的拷貝頁面內容,一樣本來是從 ds:si 到 es:di的,為什麼不指定ds和es段暫存器呢?其實是因為Linux系統設定的是平坦記憶體模式,也就是說ds和es等暫存器都為0(我是這麼理解的,如果有錯誤,希望大家指正),32位地址,通用暫存器也是32位的,所以用通用暫存器足夠表示任何一個地址了。好像在內嵌彙編中是不可以修改段暫存器的(也修改不了);
4、unsigned long get_free_page(void):得到一頁空閒的物理頁,因為要掃描所有的物理頁,所以用內嵌彙編可以提高效率。實現函式如下:
unsigned long get_free_page(void)
{
//__res是暫存器級變數,值儲存在ax暫存器中,就是說對__res的操作等於ax暫存器的操作,為效率考慮
register unsigned long __res asm("ax");
__asm__("std ; repne ; scasb\n\t"//設定方向,從字串尾部開始比較,al和di比較
"jne 1f\n\t"
"movb $1,1(%%edi)\n\t"//查詢到最後一個空閒頁面,置1表示要被引用
"sall $12,%%ecx\n\t" // 頁面數算數左移12位,表示頁面的基地址(主記憶體中)
"addl %2,%%ecx\n\t"// 在上面的基礎上加上最小記憶體1MB,則表示實際實體記憶體頁基地址
"movl %%ecx,%%edx\n\t"
"movl $1024,%%ecx\n\t"//下面清零迴圈次數
"leal 4092(%%edx),%%edi\n\t"// 把空閒頁面的最後第四個位元組起始地址賦值給edi,也即是最後一頁開始地址
"rep ; stosl\n\t"// 因為eax=0,所以這是從4092地址開始,反向清零
"movl %%edx,%%eax\n"
"1:"
:"=a" (__res)
:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
"D" (mem_map+PAGING_PAGES-1) //表示最後一個頁面的地址
:"di","cx","dx");
return __res;
}
5、功能:釋放一個addr實體地址所在的物理頁面;引數:addr,表示要所在物理頁面將要被釋放(確切的說是解引用),注意addr表示的是實體地址
函式原型:void free_page(unsigned long addr);
簡介:釋放以addr所在的一頁主記憶體,所謂釋放就是找到addr地址所在的頁面號,然後對應的對映函式值減1.(因為如果是共享頁面,那麼對映值就是大於1的,這個函式就是僅僅解除某個程序對該頁面的引用。如果對映值為1,表示只有一個程序引用,那這個就是真正徹底的釋放掉這一頁記憶體了);
6、 功能:從線性地址from所在的頁目錄項開始順序釋放size個連續頁表及所指向的實體記憶體(其實就是釋放size×4MB記憶體大小); 引數:from,線性地址;size,釋放記憶體大小;返回:成功 0 ;出錯 宕機
函式原型:int free_page_tables(unsigned long from,unsigned long size);
簡介:通過from線性地址獲取到頁目錄項號----》獲取到頁目錄項的起始地址----》根據size向上取整後右移22位得到頁表個數(多少個4MB)----》根據size的個數迴圈遍歷每個頁表項---》根據每個頁表項提供的內容得到頁表實體地址----》用from的線性地址和頁表實體地址確定一個物理頁面----》呼叫上面的函式釋放掉這個物理頁面;(如果能很熟悉線性地址轉換實體地址方法(如果不理解可以先看下http://blog.csdn.net/yuzhihui_no1/article/details/43021405),看這個函式就非常容易)
7、功能:根據from線性地址查詢到對映的實體地址,然後把to的線性地址也對映到該實體地址頁上,共享的大小為size個頁表(size×4MB)。一般用於fork()建立程序使用;引數:from,源線性地址;to,目的線性地址;size,共享頁面大小;返回:成功 0;出錯 -1(或宕機)
函式原型:int copy_page_tables(unsigned long from,unsigned long to,long size);
簡介:根據from和to這兩個線性地址分別得到頁目錄項from_dir和to_dir-----》接著根據size向上取整後右移22位得到頁表個數(也即是多少個4MB)----》根據頁目錄項內容判斷:如果目的頁表存在,則宕機(因為不能更改已存在的頁表);如果源頁表無效,則讓from_dir和to_dir分別指向下一個頁目錄項 ----》根據頁目錄項內容得到頁表實體地址:from_page_table,以及目的頁表申請得到的頁實體地址:to_page_table -----》根據申請到的頁表地址和頁面屬性,設定頁目錄項內容(頁目錄項內容開始一定為空的,要不在第三步已經宕機了)
----》 根據頁面項內容開始共享頁面,全部共享頁面都設定為只讀,並且設定對映函式mem_map[] ----》重新整理頁變快取記憶體
8、功能:把page實體記憶體對映到address線性地址處;引數:page,物理頁地址;address,線性地址;返回:成功 返回頁實體地址,出錯 0
函式原型:unsigned long put_page(unsigned long page,unsigned long address);
簡介:先判斷指定的物理頁是否在主記憶體區 ----》 判斷是否是共享頁面(對映值為1)----》根據線性地址獲取到頁目錄項 ----》根據頁目錄項內容,判斷頁表是否存在;存在就 獲取到頁表實體地址,不存在就申請一個空閒頁(當然還需要根據申請到的頁的實體地址和頁屬性設定頁目錄項內容) ----》根據線性地址中間頁表相關10位數,定位到頁表項,然後把頁表項中內容設定為引數物理頁地址page以及該頁的屬性;
9、功能:對table_entry頁表項指向的物理頁面取消防寫;引數:table_entry 頁表項
函式原型:void un_wp_page(unsigned long * table_entry);
簡介:根據引數table_entry獲取到前20位頁框地址(其實也就是物理頁地址)----》如果該物理頁在主記憶體中並且只被引用一次,那麼直接可以把只讀變成可讀可寫,退出 -----》根據上一步可以知道,該物理頁可能在核心空間或者被多次引用,那麼就在主記憶體中申請一個空閒頁 ----》如果物理頁是在主記憶體中,那麼讓對映值減去1 ----》並且把申請到頁地址和頁屬性新增到引數table_entry中 ----》最後把舊的頁面上資料拷貝到新頁面上;
其實這就是寫時複製的本質(當然,do_wp_page()函式才是被中斷呼叫的寫實複製處理函式),因為用fork()建立子程序時,會先呼叫上面copy_page_tables()函式對父程序的記憶體共享給子程序,同時設定記憶體頁面為只讀的。當一個程序準備往頁面上寫入資料時,因為寫入的是隻讀頁面,所以會產生中斷,就會呼叫這個函式,為想寫入資料的程序複製得到另外一個記憶體空間,並且新記憶體空間被設定為可寫的(開始共享的頁面,現在還是隻讀的。當下次向該頁面寫入資料時,發生中斷再呼叫這個do_wp_page()函式給該頁面取消防寫,而該函式其本質就是提前線性地址所在的頁表項傳遞給un_wp_page()函式執行;
10、寫頁面驗證,驗證address線性地址所對映到的實體地址頁是否可寫。不可寫則呼叫上一個函式up_wp_page()來建立新頁面;引數:address驗證的頁面線性地址;
函式原型:void write_verify(unsigned long address);
簡介:首先根據address線性地址獲取到頁目錄項,然後依次獲取到頁表項,判斷頁表項中後面幾位屬性欄位(倒數第一位為p位,表示是否存在記憶體中;倒數第二位為可寫位,表示頁面是否可寫;如果可寫什麼也不做,返回;如果不可寫則呼叫un_wp_page函式去處理);
11、功能:取一塊空閒頁,對映到address線性地址上;引數:address,要對映到的指定線性地址
函式原型:void get_empty_page(unsigned long address);
簡介:用get_free_page()函式獲取一頁空閒記憶體,然後用put_page(tmp,address)函式把獲取到的實體記憶體和引數address進行對映;
12、功能:把當前程序中的地址address和指定程序p中的地址address進行共享;引數:address,程序中的的地址;p,程序(也就是任務)
函式原型:static int try_to_share(unsigned long address, struct task_struct * p);
簡介:這個函式有點難理解,如果你不懂程序的話,建議可以跳開,稍微瀏覽下程序相關概念,再到回來看會相對好理解些;首先說明下address不是線性地址,所謂線性地址就是在2^32範圍內的那個地址;而這裡的address是程序中的地址,每個程序都有64MB大小地址(程序中的地址都是從0到64MB的),而每個程序中的起始線性地址為:nr×64MB(其中nr是表示該程序是第幾號程序),所以程序中的地址要轉換成線性地址,就必須要把程序的起始地址:nr×64MB 加上;nr*64MB + address 才是我們熟悉的線性地址;
首先根據程序內地址address,獲取到對應的頁目錄項指標 ====》 然後根據兩程序(current和p)的起始地址獲取到相應的頁目錄項指標 ====》 接著獲取到兩程序內地址address變換成線性地址後應該得到頁目錄項指標(通過前面兩個頁目錄項指標相加) ====》 按照老規矩依次獲取到p程序中address對應的頁表項,並做一些必要檢查(是否存在,是否乾淨)====》 同樣的方法獲取到當前程序中address對應的頁表項 ====》 把p中得到的頁表項設定為只讀,並且把該頁表項複製給當前程序的address對應的頁表項;====》 最後對他們共享的頁表項對應的物理頁表對映值進行加1,表示又有一個程序引用了該頁面(其實就是當前程序引用了)。
13、功能:這個函式是查詢程序組中是否有可以共享頁面的程序;引數:address,程序中的地址,用來給try_to_share()函式做引數
函式原型:static int share_page(unsigned long address);
簡介:判斷當前程序是否有執行的檔案,如果沒有,或者執行檔案只有一個程序在引用(即是當前程序自己在用),那就別折騰,自己退出;====》 掃描程序組,查詢引用了和當前程序一樣的執行檔案,那麼呼叫上面的函式 try_to_share()進行去共享頁面;
14、處理缺頁中斷函式;引數:error_code,出錯型別;address,產生異常的頁面線性地址;
函式原型:void do_no_page(unsigned long error_code,unsigned long address);
簡介:說實話這個函式很難,因為涉及到程序、塊裝置、檔案系統,所以等我看完檔案系統和塊裝置後,要回來修改下; 首先還是老規矩獲取到address的頁面地址,根據當前程序的起始地址,再得到addrss在程序中對應的邏輯地址;====》 如果當前程序沒有引用執行檔案,或者邏輯地址超出範圍,則申請一個新的實體地址,並且把address對映上去; =====》 嘗試給當前程序找一個共享頁面 ====》如果沒成功,則申請一個物理頁,從address對應的資料塊號(邏輯塊 在0.11中他們是一一對應的)中把資料讀取到記憶體中;====》對超出執行檔案的內容進行處理,就是全部置0;====》最後把產生缺頁中的頁面線性地址對映到前面獲取到資料的記憶體處;
15、對記憶體初始化;引數:start_men,主記憶體開始的地址;end_men,記憶體結束位置;
函式原型:void mem_init(long start_mem, long end_mem);
簡介:其實很簡單,就是對1MB~16MB物理頁面設定佔用標記,然後對主記憶體中的對映值設定為0;
16、計算記憶體空閒頁面並且顯示;
函式原型:void calc_mem(void);
簡介:迴圈統計實體記憶體空閒頁,並打印出空閒頁和總頁數;====》迴圈統計每個頁表中有多少個有效物理頁;
如果有什麼不正確之處,歡迎大家指正,一起努力,共同學習!!