最近一段時間在閱讀Linux的源代碼,想把看到的東西寫出來,覺得內存這一部分最簡單,就先寫了出來。請指正!
內存最低4K的地址是一張頁目錄(page_dir),頁目錄共1024項,每項4字節。目錄項的結構如下:
____________________________________|32-12位為頁框地址 | |U|R|p|| | |S|W| ||_________________|______ |_|_ |_|
隨後的16K,用來做了4張頁表,頁表項結構和頁目錄項結構一樣。頁表的每一項指向一個物理頁面,也就是指向內存中的一個4K大小的空間。有了這4張頁表,已經能尋址16M的內存了。下面就是在系統初始化的時候在head.s程序中設置一張頁目錄和四張頁表的代碼。此時頁目錄中僅前4項有效,正是指向位於其下面的4張頁表,而這4張頁表尋址了內存的最低16M。
198 setup_paging:199 movl 24*5,%ecx /* 5 pages - pg_dir+4 page tables */200 xorl %eax,%eax201 xorl %edi,%edi /* pg_dir is at 0x000 */202 cld;rep;stosl203 movl $pg0+7,_pg_dir /* set present bit/user r/w */204 movl $pg1+7,_pg_dir+4 /* --------- " " --------- */205 movl $pg2+7,_pg_dir+8 /* --------- " " --------- */206 movl $pg3+7,_pg_dir+12 /* --------- " " --------- */207 movl $pg3+4092,%edi208 movl xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */209 std210 1: stosl /* fill pages backwards - more efficient :-) */211 subl x1000,%eax212 jge 1b
以後每次有fork新進程,都要為新進程分配內存。但具體是怎麼做的呢,我也想知道,一起看吧。當執行fork時,它使用int0x80調用sys_fork函數,sys_fork的代碼位於system_call.s中,很短如下:
208 _sys_fork:209 call _find_empty_process210 testl %eax,%eax211 js 1f212 push %gs213 pushl %esi214 pushl %edi215 pushl %ebp216 pushl %eax217 call _copy_process218 addl ,%esp219 1: ret
看到其中調用了兩個函數,find_empty_process and copy_process,這兩個函數在fork.c文件裡實現的。find_empty_process是為將要創建的新進程找一個pid,保存在last_pid裡,然後調用copy_process,這是sys_fork真正的主程序,其中有如此句:
77 p = (strUCt task_struct *) get_free_page();
先為新進程分配一張物理頁面,用來存放進程的PCB結構,即task_struct結構。光給新進程一張物理頁面來存放它的task_struct,顯然是不能滿足它的。我們知道,在創建之初,新進程是和其父進程共享代碼和數據的。這是人為定的,不過這樣的好處不言而喻。因此在創建的時候就沒有必要將其代碼和數據全部copy到新內存地址裡,而只為新進程創建頁目錄項和頁表就可以了。代碼如下:
115 if (copy_mem(nr,p)) { /*copy_mem調用memory.c裡的copy_page_tables*/116 task[nr] = NULL;117 free_page((long) p);118 return -EAGAIN;119 }
copy_mem為新進程分配頁表空間,並把父進程的頁表內容copy到新進程的頁表空間裡,這樣新進程的頁表的每一項指向的物理頁面和其父進程頁表的相應每一項指向的物理頁面是一樣的。少說了一些,不能只copy頁表就完事了。32位線性地址轉換為物理地址的時候,最先要找到32位線性地址對應的頁目錄項,再用頁目錄項找到頁表地址。新進程有了自己的頁表,並且頁表也都指向了物理地址,現在少的就是頁目錄項了。新進程在創建的時候,在4G線性空間裡給其分配了64M的線性空間,是通過設置LDT來完成的:
130 set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
這64M的線性地址是從nr*64M的地址處開始的,這個地址正好可以被映射到頁目錄裡的一項,這項的地址是:((nr*64M)>>20)&0xffc。只要從這裡開始,在頁目錄裡建一些頁目錄項,指向新創建的進程的頁表地址(copy_mem調用copy_page_tables()來做的)。到這裡,copy_mem的工作可以說是完成了,不過一定不能少了這一句:
177 this_page &= ~2; (memory.c)
由於新進程和其父進程共享物理內存頁面,因此把這些物理頁面重新都設成只讀是必要的。上面這句是放在copy_page_tables函數裡面的循環中的。copy_mem主要是靠調用這個程序來完成工作的。分析到這裡,我終於可以小舒一口氣了。不如回顧一下:系統初始化的時候在內存起始處建一張頁目錄(page_dir),以後所有的進程都使用這張頁目錄。並為系統建了4張頁表。以後每有新進程產生,便為之分配空間存放PCB(即struct task_struct),然後為之通過復制父進程的頁表來創建自己的頁表,並創建相應的頁目錄項。
程序運行了,問題又來了。終於讀到了“寫時復制”和請求調頁的部分。當程序訪問的線性地址沒有被映射到一個物理頁面,或欲寫操作的線性地址映射的物理頁面僅是只讀,都會產生一個頁異常,然後就會轉去頁異常中斷處理程序(int 14)執行,頁異常中斷處理程序(page.s)如下:
14 _page_fault: 15 xchgl %eax,(%esp) 16 pushl %ecx 17 pushl %edx 18 push %ds 19 push %es 20 push %fs 21 movl x10,%edx 22 mov %dx,%ds 23 mov %dx,%es 24 mov %dx,%fs 25 movl %cr2,%edx 26 pushl %edx 27 pushl %eax 28 testl ,%eax 29 jne 1f 30 call _do_no_page 31 jmp 2f 32 1: call _do_wp_page 33 2: addl ,%esp 34 pop %fs 35 pop %es 36 pop %ds 37 popl %edx 38 popl %ecx 39 popl %eax 40 iret
根據error_code判斷是缺頁還是寫保護引起的異常,然後去執行相應的處理程序段,先看寫保護的處理吧。
247 void do_wp_page(unsigned long error_code,unsigned long address)248 {249 #if 0250 /* we cannot do this yet: the estdio library writes to code space */251 /* stupid, stupid. I really want the libc.a from GNU */252 if (CODE_SPACE(address))253 do_exit(SIGSEGV);254 #endif255 un_wp_page((unsigned long *)256 (((address>>10) & 0xffc) + (0xfffff000 &257 *((unsigned long *) ((address>>20) &0xffc)))));258259 }
程序就一個函數調用,很少有這麼簡單的函數,哈哈!address很顯然是程序想要訪問但引起出錯的線性地址了。(0xfffff000&*((unsigned long *)((address>>20)&0xffc))計算出32位線性地址對應頁表的地址,再加上一個((address>>10) & 0xffc),就是加上頁表內的偏移量,即得到頁表內的一個頁表項。看un_wp_page()就更明白了。
221 void un_wp_page(unsigned long * table_entry)222 {223 unsigned long old_page,new_page;224225 old_page = 0xfffff000 & *table_entry;226 if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) {227 *table_entry = 2;228 invalidate();229 return;230 }231 &nb
232 oom();233 if (old_page >= LOW_MEM)234 mem_map[MAP_NR(old_page)]--;235 *table_entry = new_page 7;236 invalidate();237 copy_page(old_page,new_page);238 }
225-229做了個判斷,如果此物理頁面沒有被共享,則只要將可寫位置1(227)。不然就進入231行去。
在物理內存中分配一頁空間,把原頁面的內容copy到新頁面裡(copy_page),再把那個引起出錯的address映射到這個新頁面的物理地址上去(235行)。至此,寫保護出錯的處理完成了,可以返回去執行原進程裡引起出錯的那條指令了。
上面所述,就是所謂的“寫時復制(copy on write)”。如果是缺頁異常的話,則執行do_no_page,最簡單的辦法就是直接申請一張物理頁面,對應到這個引起出錯的address,如下:
372 address &= 0xfffff000;373 tmp = address - current->start_code;374 if (!current->executable tmp >= current->end_data) {375 get_empty_page(address);376 return;377 }
如果這樣了之,那也太不負責任了,只是在!current->executable tmp >= current->end_data的情況下,才這樣做。這是怎樣的情況呢?!current->executable有待閱讀,tmp >= current->end_data很簡單,在程序體已全部讀入內存後,這可能是動態內存分配所要求的內存空間。否則就嘗試去和別的進程共享一下,如下:
378 if (share_page(tmp))379 return;
如果共享不成,那也只好自己申請一張頁面了,如下:
380 if (!(page = get_free_page()))381 oom();
一張頁面4K大小,那就到設備上去讀4K大小的程序內容到內存,根據current->executable,可以在設備上找到缺頁對應程序的相應位置。
382 /* remember that 1 block is used for header */383 block = 1 + tmp/BLOCK_SIZE;384 for (i=0 ; i<4 ; block++,i++)385 nr[i] = bmap(current->executable,block);386 bread_page(page,current->executable->i_dev,nr);
判斷讀入4K是否大於程序長度,是的話,則把多出的部分清零。
387 i = tmp + 4096 - current->end_data;388 tmp = page + 4096;389 while (i-- > 0) {390 tmp--;391 *(char *)tmp = 0;392 }
最後不能忘了把新頁面的物理地址和出錯的線性地址address相對應,形成映射。
393 if (put_page(page,address))394 return;
do_no_page,就是操作系統理論中的請求調頁。終於明白,原來那麼多的操作系統書籍用那麼大堆的紙張所述的東西,真正寫起操作系統來,用幾小函數就把它們完成了。內存分配出去,當進程運行結束,回收是必要的。其實這些也是簡單的,因為有一個數組,就是下面的:
43 #define LOW_MEM 0x100000 44 #define PAGING_MEMORY (15*1024*1024) 45 #define PAGING_PAGES (PAGING_MEMORY>>12)
57 static unsigned char mem_map [ PAGING_PAGES ] = ;
可以看到,數組項數是除去最低1M內存後可以分成的頁面數,也就是可以用的物理內存頁面。系統在初始化的時候把還沒有被使用的內存物理頁面對應的項置為了0,初始代碼如下:
399 void mem_init(long start_mem, long end_mem)400 {401 int i;402403 HIGH_MEMORY = end_mem;404 for (i=0 ; i405 mem_map[i] = USED;406 i = MAP_NR(start_mem);407 end_mem -= start_mem;408 end_mem >>= 12;409 while (end_mem-->0)410 mem_map[i++]=0;411 }
其實前面所有的申請內存的程序裡都最終使用了一個函數get_free_page(),不管申請多少的內存,最終還是要按頁面來申請:
63 unsigned long get_free_page(void) 64 { 65 register unsigned long __res asm("ax"); 66 67 __asm__("std ; repne ; scasb\n\t" 68 "jne 1f\n\t" 69 "movb ,1(%%edi)\n\t" 70 "sall ,%%ecx\n\t" 71 "addl %2,%%ecx\n\t" 72 "movl %%ecx,%%edx\n\t" 73 "movl 24,%%ecx\n\t" 74 "leal 4092(%%edx),%%edi\n\t" 75 "rep ; stosl\n\t" 76 "movl %%edx,%%eax\n" 77 "1:" 78 :"=a" (__res) 79 :"" (0),"i" (LOW_MEM),"c" (PAGING_PAGES), 80 "D" (mem_map+PAGING_PAGES-1) 81 :"di","cx","dx"); 82 return __res; 83 }
這個函數就是在物理內存中找一張沒有使用的頁面並返回其物理地址。這是一段gcc內聯匯編,它在mem_map數組中的最後一項一直向前找,只要找一項的值不為0,則用這個數組下標計算出物理地址返回,並把那一項的值設為1。用下標計算物理地址的方法我想是這樣的:index*4096+LOW_MEN (std;repne;scasb,這三句是依次檢查mem_map裡的每一項的值,如果全部不為0,也即沒有物理內存可以用,立即返回0。movb ,1(%%edi)這句就是把mem_map數組裡找到的可用的一項的標志設為1。此時ecx裡的值就是數組下標,因此sall ,%%ecx就是index*4096,addl %2,%%ecx即把剛才的index*4096+LOW_MEM。73,74,5三句是把相應的物理內存空間內容全部清0。movl %%edx,%%eax顯然是返回值了)。
有件事一定要做,那就是在返回之前把那個物理頁面的內容全部清0。清0的事情讓get_free_page做了,回收就簡單了,只要把mem_map數組的相應項置為0就可以了,從下面可以看出來,free_page確實只做了這件事:
89 void free_page(unsigned long addr) 90 { 91 if (addr < LOW_MEM) return; 92 if (addr >= HIGH_MEMORY) 93 panic("trying to free nonexistent page"); 94 addr -= LOW_MEM; 95 addr >>= 12; 96 if (mem_map[addr]--) return; 97 mem_map[addr]=0; 98 panic("trying to free free page"); 99 }
進程退出時,會調用sys_exit,sys_exit只是調用了一下do_exit,回收內存的工作就在這裡完成的。
106 free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));107 free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
free_page_tables釋放進程的代碼段和數據段占用的內存,它內部使用循環,調用free_page完成最終的工作。
有件事一定要做,那就是在返回之前把那個物理頁面的內容全部清0。清0的事情讓get_free_page做了,回收就簡單了,只要把mem_map數組的相應項置為0就可以了,從下面可以看出來,free_page確實只做了這件事:
89 void free_page(unsigned long addr) 90 { 91 if (addr < LOW_MEM) return; 92 if (addr >= HIGH_MEMORY) 93 panic("trying to free nonexistent page"); 94 addr -= LOW_MEM; 95 addr >>= 12; 96 if (mem_map[addr]--) return; 97 mem_map[addr]=0; 98 panic("trying to free free page"); 99 }
進程退出時,會調用sys_exit,sys_exit只是調用了一下do_exit,回收內存的工作就在這裡完成的。
106 free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));107 free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
free_page_tables釋放進程的代碼段和數據段占用的內存,它內部使用循環,調用free_page完成最終的工作。
有件事一定要做,那就是在返回之前把那個物理頁面的內容全部清0。清0的事情讓get_free_page做了,回收就簡單了,只要把mem_map數組的相應項置為0就可以了,從下面可以看出來,free_page確實只做了這件事:
89 void free_page(unsigned long addr) 90 { 91 if (addr < LOW_MEM) return; 92 if (addr >= HIGH_MEMORY) 93 panic("trying to free nonexistent page"); 94 addr -= LOW_MEM; 95 addr >>= 12; 96 if (mem_map[addr]--) return; 97 mem_map[addr]=0; 98 panic("trying to free free page"); 99 }
進程退出時,會調用sys_exit,sys_exit只是調用了一下do_exit,回收內存的工作就在這裡完成的。
106 free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));107 free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
free_page_tables釋放進程的代碼段和數據段占用的內存,它內部使用循環,調用free_page完成最終的工作。