草庐IT

Linux 0.11源码阅读笔记-内存管理

LazyFish 2023-03-28 原文

内存管理

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 ; scasb\n\t"   // 置方向位,al(0)与对应每个页面的(di)内容比较
	"jne 1f\n\t"                    // 如果没有等于0的字节,则跳转结束(返回0).
	"movb $1,1(%%edi)\n\t"          // 1 => [1+edi],将对应页面内存映像bit位置1.
	"sall $12,%%ecx\n\t"            // 页面数*4k = 相对页面其实地址
	"addl %2,%%ecx\n\t"             // 再加上低端内存地址,得页面实际物理起始地址
	"movl %%ecx,%%edx\n\t"          // 将页面实际其实地址->edx寄存器。
	"movl $1024,%%ecx\n\t"          // 寄存器ecx置计数值1024
	"leal 4092(%%edx),%%edi\n\t"    // 将4092+edx的位置->dei(该页面的末端地址)
	"rep ; stosl\n\t"               // 将edi所指内存清零(反方向,即将该页面清零)
	"movl %%edx,%%eax\n"            // 将页面起始地址->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 %p\n",page,address);
	if (mem_map[(page-LOW_MEM)>>12] != 1)
		printk("mem_map disagrees with %p at %p\n",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);		//复制物理页
}

有关Linux 0.11源码阅读笔记-内存管理的更多相关文章

  1. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

    作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

  2. ruby - i18n Assets 管理/翻译 UI - 2

    我正在使用i18n从头开始​​构建一个多语言网络应用程序,虽然我自己可以处理一大堆yml文件,但我说的语言(非常)有限,最终我想寻求外部帮助帮助。我想知道这里是否有人在使用UI插件/gem(与django上的django-rosetta不同)来处理多个翻译器,其中一些翻译器不愿意或无法处理存储库中的100多个文件,处理语言数据。谢谢&问候,安德拉斯(如果您已经在ruby​​onrails-talk上遇到了这个问题,我们深表歉意) 最佳答案 有一个rails3branchofthetolkgem在github上。您可以通过在Gemfi

  3. ruby-on-rails - Ruby 中的内存模型 - 2

    ruby如何管理内存。例如:如果我们在执行过程中采用C程序,则以下是内存模型。类似于这个ruby如何处理内存。C:__________________|||stack|||------------------||||------------------|||||Heap|||||__________________|||data|__________________|text|__________________Ruby:? 最佳答案 Ruby中没有“内存”这样的东西。Class#allocate分配一个对象并返回该对象。这就是程序

  4. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

  5. ruby-on-rails - 获取 inf-ruby 以使用 ruby​​ 版本管理器 (rvm) - 2

    我安装了ruby​​版本管理器,并将RVM安装的ruby​​实现设置为默认值,这样'哪个ruby'显示'~/.rvm/ruby-1.8.6-p383/bin/ruby'但是当我在emacs中打开inf-ruby缓冲区时,它使用安装在/usr/bin中的ruby​​。有没有办法让emacs像shell一样尊重ruby​​的路径?谢谢! 最佳答案 我创建了一个emacs扩展来将rvm集成到emacs中。如果您有兴趣,可以在这里获取:http://github.com/senny/rvm.el

  6. ruby-on-rails - 事件管理员日期过滤器日期格式自定义 - 2

    是否有简单的方法来更改默认ISO格式(yyyy-mm-dd)的ActiveAdmin日期过滤器显示格式? 最佳答案 您可以像这样为日期选择器提供额外的选项,而不是覆盖js:=f.input:my_date,as::datepicker,datepicker_options:{dateFormat:"mm/dd/yy"} 关于ruby-on-rails-事件管理员日期过滤器日期格式自定义,我们在StackOverflow上找到一个类似的问题: https://s

  7. UE4 源码阅读:从引擎启动到Receive Begin Play - 2

    一、引擎主循环UE版本:4.27一、引擎主循环的位置:Launch.cpp:GuardedMain函数二、、GuardedMain函数执行逻辑:1、EnginePreInit:加载大多数模块int32ErrorLevel=EnginePreInit(CmdLine);PreInit模块加载顺序:模块加载过程:(1)注册模块中定义的UObject,同时为每个类构造一个类默认对象(CDO,记录类的默认状态,作为模板用于子类实例创建)(2)调用模块的StartUpModule方法2、FEngineLoop::Init()1、检查Engine的配置文件找出使用了哪一个GameEngine类(UGame

  8. ruby - 安装libv8(3.11.8.13)出错,Bundler无法继续 - 2

    运行bundleinstall后出现此错误:Gem::Package::FormatError:nometadatafoundin/Users/jeanosorio/.rvm/gems/ruby-1.9.3-p286/cache/libv8-3.11.8.13-x86_64-darwin-12.gemAnerroroccurredwhileinstallinglibv8(3.11.8.13),andBundlercannotcontinue.Makesurethat`geminstalllibv8-v'3.11.8.13'`succeedsbeforebundling.我试试gemin

  9. LC滤波器设计学习笔记(一)滤波电路入门 - 2

    目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称

  10. ruby - (Ruby || Python) 窗口管理器 - 2

    我想用这两种语言中的任何一种(最好是ruby​​)制作一个窗口管理器。老实说,除了我需要加载某种X模块外,我不知道从哪里开始。因此,如果有人有线索,如果您能指出正确的方向,那就太好了。谢谢 最佳答案 XCB,X的下一代API使用XML格式定义X协议(protocol),并使用脚本生成特定语言绑定(bind)。它在概念上与SWIG类似,只是它描述的不是CAPI,而是X协议(protocol)。目前,C和Python存在绑定(bind)。理论上,Ruby端口只是编写一个从XML协议(protocol)定义语言到Ruby的翻译器的问题。生

随机推荐