歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> 關於Linux

Linux操作系統內存管理的源碼實現

最近一段時間在閱讀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,%eax
201     xorl %edi,%edi         /* pg_dir is at 0x000 */
202     cld;rep;stosl
203     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,%edi
208     movl xfff007,%eax       /* 16Mb - 4096 + 7 (r/w user,p) */
209     std
210 1:   stosl          /* fill pages backwards - more efficient :-) */
211     subl x1000,%eax
212     jge 1b

以後每次有fork新進程,都要為新進程分配內存。但具體是怎麼做的呢,我也想知道,一起看吧。當 執行fork時,它使用int0x80調用sys_fork函數,sys_fork的代碼位於system_call.s中,很短如下:

208 _sys_fork:
209     call _find_empty_process
210     testl %eax,%eax
211     js 1f
212     push %gs
213     pushl %esi
214     pushl %edi
215     pushl %ebp
216     pushl %eax
217     call _copy_process
218     addl ,%esp
219 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 0
250 /* 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 #endif
255     un_wp_page((unsigned long *)
256         (((address>>10) & 0xffc) + (0xfffff000 &
257         *((unsigned long *) ((address>>20) &0xffc)))));
258
259 }

程序就一個函數調用,很少有這麼簡單的函數,哈哈!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;
224
225     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;
402
403     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完成最終 的工作。

Copyright © Linux教程網 All Rights Reserved