- A+
内存管理
Linux内核使用段页式内存管理方式。
- 内存池
物理页:物理空闲内存被划分为固定大小(4k)的页
内存池:所有空闲物理页组成内存池,以页为单位进行分配回收。并通过位图记录了每个物理页是否空闲,位图下标对应物理页号。
- 分页内存管理
虚拟页:进程虚地址空间被划分为固定大小(4k)的页
分页内存管理:通过页目录和页表维护进程虚拟页号到物理页号的映射。设置好页目录、页表之后,虚拟地址到物理地址之间的转换通过内存管理单元(MMU)自动完成转换。若访问的虚拟页没有实际分配物理页,则放生缺页中断,内核会为其分配物理页。
- 分段内存管理
分段:进程虚地址空间被划分为多个逻辑段,代码段、数据段、栈段等,每个段有一个段号。进程代码不直接使用虚拟地址,而是段号+段内偏移的二维逻辑地址。
分段内存管理:通过段表维护每个段的信息,段表项包括段基址和段限长。设置好段表之后,段号+段内偏移二维逻辑地址到虚拟线性地址的转换由MMU单元自动完成。
- 相关代码文件
page.s:仅包含内存缺页中断处理程序
memory.c:内存管理的核心文件,用于内存池的初始化操作、页目录和页表的管理和内核其他部分对内存的申请处理过程。
物理内存管理
除去以被内核占用的内存外,剩余为占用内存会使用内存池进行管理,用于动态的分配和回收。
内存池初始化
mem_init初始化空闲内存。将空闲内存划分为4k大小页,并在位图mem_map中标记为空闲。位图中还包含物理页的引用计数,支持内存共享机制。
void mem_init(long start_mem, long end_mem) { int i; HIGH_MEMORY = end_mem; # 在位图中,设置所有页面为占用状态 for (i=0 ; i<PAGING_PAGES ; i++) mem_map[i] = USED; # 在位图中,将内核未使用的空闲页面设置为空闲状态,start_mem为空闲内存起始地址 i = MAP_NR(start_mem); // 主内存区起始位置处页面号 end_mem -= start_mem; end_mem >>= 12; // 主内存区中的总页面数 while (end_mem-->0) mem_map[i++]=0; // 主内存区页面对应字节值清零 }
内存分配回收
内核代码通过get_free_page和free_page函数分配和回收物理内存页。
- 分配
get_free_page函数用于分配物理页。在位图中查找空闲物理页,并标记为占用,然后返回一个空闲的页物理地址。
// 不要陷入代码细节 unsigned long get_free_page(void) { register unsigned long __res asm("ax"); __asm__("std ; repne ; scasbnt" // 置方向位,al(0)与对应每个页面的(di)内容比较 "jne 1fnt" // 如果没有等于0的字节,则跳转结束(返回0). "movb $1,1(%%edi)nt" // 1 => [1+edi],将对应页面内存映像bit位置1. "sall $12,%%ecxnt" // 页面数*4k = 相对页面其实地址 "addl %2,%%ecxnt" // 再加上低端内存地址,得页面实际物理起始地址 "movl %%ecx,%%edxnt" // 将页面实际其实地址->edx寄存器。 "movl $1024,%%ecxnt" // 寄存器ecx置计数值1024 "leal 4092(%%edx),%%edint" // 将4092+edx的位置->dei(该页面的末端地址) "rep ; stoslnt" // 将edi所指内存清零(反方向,即将该页面清零) "movl %%edx,%%eaxn" // 将页面起始地址->eax(返回值) "1:" :"=a" (__res) :"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES), "D" (mem_map+PAGING_PAGES-1) ); return __res; // 返回空闲物理页面地址(若无空闲页面则返回0). }
- 回收
free_page函数用于释放物理页。释放物理地址addr处的物理页,并在位图中标记为未占用状态。
void free_page(unsigned long addr) { // 判断地址是否在合法范围内 if (addr < LOW_MEM) return; if (addr >= HIGH_MEMORY) panic("trying to free nonexistent page"); addr -= LOW_MEM; addr >>= 12; if (mem_map[addr]--) return; mem_map[addr]=0; panic("trying to free free page"); }
分页内存管理
- 多级页表
多级页表用于实现虚拟页到物理页的映射,进程基于多级页表管理其占用的物理内存页。
使用单级页表实现虚拟页到物理页的映射会浪费较多的内存空间,将单级页表划分为固定的大小(4k)的页表,并使用页目录登记页表,从而实现两级页表,进一步可实现多级页表。使用多级页表的好处在于节省空闲页表占用的内存空间,当4k大小页表没有页项使用时,可以不为其申请内存空间。
- 线性虚拟地址翻译
线性地址可以划分为页目录项、页表项、页内偏移。
页目录项:作为下标访问页目录表项,表项记录页表信息
页表项:作为下标访问页表项,也表项记录物理页信息
页内偏移:作为物理页内偏移访问具体的物理地址单元
- 复制页表
copy_page_tables函数用于复制当前进程的页目录和页表。首先会申请内存作为页目录和也表的存储空间,然后进行复制,复制后的两个进程的目标共享实际物理内存。fork新进程程时,会调用该函数为新进程从原进程复制页表。
int copy_page_tables(unsigned long from,unsigned long to,long size) { unsigned long * from_page_table; unsigned long * to_page_table; unsigned long this_page; unsigned long * from_dir, * to_dir; unsigned long nr; if ((from&0x3fffff) || (to&0x3fffff)) panic("copy_page_tables called with wrong alignment"); from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */ to_dir = (unsigned long *) ((to>>20) & 0xffc); size = ((unsigned) (size+0x3fffff)) >> 22; // 第一层循环处理页目录 for( ; size-->0 ; from_dir++,to_dir++) { if (1 & *to_dir) panic("copy_page_tables: already exist"); if (!(1 & *from_dir)) continue; from_page_table = (unsigned long *) (0xfffff000 & *from_dir); if (!(to_page_table = (unsigned long *) get_free_page())) return -1; /* Out of memory, see freeing */ *to_dir = ((unsigned long) to_page_table) | 7; nr = (from==0)?0xA0:1024; // 第二层循环处理页表 for ( ; nr-- > 0 ; from_page_table++,to_page_table++) { this_page = *from_page_table; if (!(1 & this_page)) continue; this_page &= ~2; *to_page_table = this_page; if (this_page > LOW_MEM) { *from_page_table = this_page; this_page -= LOW_MEM; this_page >>= 12; mem_map[this_page]++; //增加物理页引用计数 } } } invalidate(); return 0; }
- 分配物理页
put_page函数为指定虚拟页分配物理页,并在页表中登记映射关系。
//为进程虚页分配分配物理页,主要过程 //1. 调用get_free_page分配一个物理页 //2. 调用put_page在页表中修改页项,建立虚页到物理页的映射 void get_empty_page(unsigned long address) { unsigned long tmp; // 如果不能取得有一空闲页面,或者不能将所取页面放置到指定地址处,则显示内存不够信息。 if (!(tmp=get_free_page()) || !put_page(tmp,address)) { free_page(tmp); /* 0 is ok - ignored */ oom(); } } //将物理页映射到地址address中 unsigned long put_page(unsigned long page,unsigned long address) { unsigned long tmp, *page_table; /* NOTE !!! This uses the fact that _pg_dir=0 */ if (page < LOW_MEM || page >= HIGH_MEMORY) printk("Trying to put page %p at %pn",page,address); if (mem_map[(page-LOW_MEM)>>12] != 1) printk("mem_map disagrees with %p at %pn",page,address); page_table = (unsigned long *) ((address>>20) & 0xffc); if ((*page_table)&1) page_table = (unsigned long *) (0xfffff000 & *page_table); else { if (!(tmp=get_free_page())) return 0; *page_table = tmp|7; page_table = (unsigned long *) tmp; } page_table[(address>>12) & 0x3ff] = page | 7; //登记页表项 /* no need for invalidate */ return page; }
- 释放物理页
free_page_tables函数释放连续一到多个虚拟页,并修改页表。
int free_page_tables(unsigned long from,unsigned long size) { unsigned long *pg_table; unsigned long * dir, nr; if (from & 0x3fffff) panic("free_page_tables called with wrong alignment"); if (!from) panic("Trying to free up swapper memory space"); size = (size + 0x3fffff) >> 22; dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */ for ( ; size-->0 ; dir++) { if (!(1 & *dir)) continue; pg_table = (unsigned long *) (0xfffff000 & *dir); // 取页表地址 for (nr=0 ; nr<1024 ; nr++) { if (1 & *pg_table) // 若该项有效,则释放对应页。 free_page(0xfffff000 & *pg_table); *pg_table = 0; // 该页表项内容清零。 pg_table++; // 指向页表中下一项。 } free_page(0xfffff000 & *dir); // 释放该页表所占内存页面。 *dir = 0; // 对应页表的目录项清零 } invalidate(); // 刷新页变换高速缓冲。 return 0; }
分段内存管理
虚拟内存被划分为多个逻辑段,代码段、只读数据段等,不同数据段的属性不同,方便管理和保护安全。
全局描述符表(GDT)和局部描述符表(LDT)用于记录段信息,包含段基址和段限长等。GDT用于记录内核使用的各种数据段,仅有一个;LDT用于记录进程使用的各种数据段,一个进程对应一个。
寄存器GDTR和LDTR分别用于存储GDT首地址和当前运行进程的LDT首地址。运行于用户态时,地址翻译使用LDTR寄存器指向的进程段表;运行于内核态时,地址翻译使用LDTR寄存器指向的内核段表。
段页式内存管理
前面分别介绍了分页内存管理和分段内存管理,及两者各自地址翻译过程,此处总结linux段页式内存翻译的整个流程,并介绍一些相关的寄存器和TLB快表。
地址翻译过程主要分为两个部分:段+偏移二维逻辑地址转化为虚拟线性地址;虚拟线性地址转化为物理地址。第一部分翻译过程依赖数据结构GDT或LDT,其中记录了段信息;第二部分翻译过程依赖页表数据结构,记录了虚拟页到物理页的映射关系,CR3寄存器存储当前进程页目录地址。
-
MMU:设置好寄存器GDTR、LDTR、CR3寄存器后,MMU内存管理单元只懂执行地址翻译过程。
-
TLB:多级页表导致地址翻译过程较慢,使用TLB快表可缓存页表项,加快地址翻译过程。
页面出错异常
缺页或者写时拷贝会都会引起页面出错异常(page_fault int14),但错处码不同。page_fault中断处理函数根据出错码调用do_no_page处理缺页中断,或者调用do_wp_page处理写时拷贝。
缺页处理
进程访问虚地址内存时,若未分配物理内存,将导致页面出错异常(page_fault int14),并调用异常处理函数do_no_page()
do_no_page将为虚拟页分配物理页,并从磁盘调入相应数据(若该虚页对应磁盘数据)。
void do_no_page(unsigned long error_code,unsigned long address) { int nr[4]; unsigned long tmp; unsigned long page; int block,i; address &= 0xfffff000; tmp = address - current->start_code; if (!current->executable || tmp >= current->end_data) { get_empty_page(address); return; } if (share_page(tmp)) return; if (!(page = get_free_page())) oom(); //执行映像文件中(外存中),读入内存块对应的数据 /* remember that 1 block is used for header */ block = 1 + tmp/BLOCK_SIZE; for (i=0 ; i<4 ; block++,i++) nr[i] = bmap(current->executable,block); bread_page(page,current->executable->i_dev,nr); //文件末尾数据可能不足一个内存块,剩下的内存空间清0 i = tmp + 4096 - current->end_data; tmp = page + 4096; while (i-- > 0) { tmp--; *(char *)tmp = 0; } // 最后把引起缺页异常的一页物理页面映射到指定线性地址address处。若操作成功 // 就返回。否则就释放内存页,显示内存不够。 if (put_page(page,address)) return; free_page(page); oom(); }
写时拷贝
fork新进程时,父子进程共享相同的物理内存页,并设置共享内存页只读。当父子进程中的一个写共享内存时,将导致页面出错异常(page_fault int14),并调用异常处理函数do_wp_page()处理。
do_wp_page会对共享内存页取消共享,并复制出一个新的内存页,使用父子进程各拥有一份自己的物理页面,可正常读写。
void do_wp_page(unsigned long error_code,unsigned long address) { #if 0 /* we cannot do this yet: the estdio library writes to code space */ /* stupid, stupid. I really want the libc.a from GNU */ if (CODE_SPACE(address)) do_exit(SIGSEGV); #endif // 调用上面函数un_wp_page()来处理取消页面保护。 un_wp_page((unsigned long *) (((address>>10) & 0xffc) + (0xfffff000 & *((unsigned long *) ((address>>20) &0xffc))))); } // 取消保护页函数 void un_wp_page(unsigned long * table_entry) { unsigned long old_page,new_page; old_page = 0xfffff000 & *table_entry; if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) { *table_entry |= 2; invalidate(); return; } if (!(new_page=get_free_page())) //分配新页 oom(); if (old_page >= LOW_MEM) mem_map[MAP_NR(old_page)]--; *table_entry = new_page | 7; invalidate(); copy_page(old_page,new_page); //复制物理页 }