歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux綜合 >> 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,%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完成最終的工作。

 

 



  這個函數就是在物理內存中找一張沒有使用的頁面並返回其物理地址。這是一段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完成最終的工作。

 

 



  這個函數就是在物理內存中找一張沒有使用的頁面並返回其物理地址。這是一段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