linux内存管理学习笔记

版权声明:本文为CSDN博主「ashimida@」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lidan113lidan/article/details/44887505
更多内容可关注微信公众号

##物理内存
物理内存是按照结点->内存域的方式划分的,每个结点关联到系统中的一个处理器,在内核中表示为pg_data_t。各个结点又划分为多个内存域,在内核中表示为zone,是物理内存的进一步细分,而物理内存的最小粒度为物理页(页帧/页框),在内核中表示为page,先看数据结构(部分结构省略):

typedef struct pglist_data {
	//代表这个结点中的所有内存域,一个结点中能划分的
	//内存域个数是有上线的,MAX_NR_ZONES
	struct zone node_zones[MAX_NR_ZONES];
	//备用内存域,貌似是给一些必然不能失败的内存分配预留的??
	struct zonelist node_zonelists[MAX_ZONELISTS];
	//结点目前划分了几个内存域
	int nr_zones;
	//指向当前所有物理页链表的指针
	struct page *node_mem_map;
	//在内核启动时,留给自举内存分配器用的,这个说白了
	//就是个简单的内存分配器,在启动时内存管理模块还不可用
	//先临时用这个简单的内存分配器
	struct bootmem_data *bdata;
	spinlock_t node_size_lock;
	//当前结点的第一个页帧的逻辑编号,系统中所有结点的
	//所有页帧是一次编号的,每个页帧的号码都是全局唯一的。
	//在UMA系统中总是0,因为整个系统只有一个节点
	//在NUMA系统中可能不是0,NUMA/UMA后续介绍。
	unsigned long node_start_pfn;
	//当前结点中的页帧的总数
	unsigned long node_present_pages; 
	//以页帧为单位的结点跨度(长度)(>=node_present_pages),因为中间可能有空洞
	unsigned long node_spanned_pages; 
	//当前结点在全局来看的结点id
	int node_id;
	wait_queue_head_t kswapd_wait;
	//struct task_struct *kswapd;
	//交换守护进程(swap deamon)的等待队列,在将页帧换出的时候会用到后续介绍
	struct task_struct *kswapd;	
	int kswapd_max_order;
	enum zone_type classzone_idx;
} pg_data_t;
struct zone {
	unsigned long watermark[NR_WMARK];
	unsigned long percpu_drift_mark;
	//备份页,用于一些无论如何也不能失败的关键性内存分配
	unsigned long		lowmem_reserve[MAX_NR_ZONES];
	unsigned long		dirty_balance_reserve;
	//用于每个cpu的冷热帧列表(高速缓存中的页帧称为热的)
	struct per_cpu_pageset __percpu *pageset;
	spinlock_t		lock;
	int  all_unreclaimable;
	//用于实现伙伴系统,每个数组元素,代表某种固定长度的
	//连续内存区
	struct free_area	free_area[MAX_ORDER];
	unsigned long		*pageblock_flags;
	unsigned int		compact_considered;
	unsigned int		compact_defer_shift;
	int			compact_order_failed;
	//ZONE_PADDING是用来隔离此数据结构的,因为对zone的访问
	//非常频繁,在不同cpu上同时访问会用到两个锁zone->lock
	//和zone->lru_lock,如果数据保存在cpu的高速缓存中,会处理
	//的更快,高速缓存分为行,每行负责不同的内存区,内核用
	//ZONE_PADDING田中,以确保每个自旋锁都处于自身的缓存行中
	//内部用____cacheline_internodealigned_in_smp关键字以
	//实现最优的高速缓存对齐方式。
	ZONE_PADDING(_pad1_)
	spinlock_t		lru_lock;
	struct lruvec		lruvec;
	struct zone_reclaim_stat reclaim_stat;
	unsigned long		pages_scanned;	
	//当前内存域的状态
	unsigned long		flags;		  
	//当前内存域的统计信息
	atomic_long_t		vm_stat[NR_VM_ZONE_STAT_ITEMS];
	unsigned int inactive_ratio;
	ZONE_PADDING(_pad2_)
	//实现一个等待队列,供需要等待某一页可用的进程使用。
	wait_queue_head_t	* wait_table;
	unsigned long		wait_table_hash_nr_entries;
	unsigned long		wait_table_bits;
	//指向内存域的父结点
	struct pglist_data	*zone_pgdat;
	//内存域的第一个页帧编号
	unsigned long		zone_start_pfn;
	//内存域中的页数跨度
	unsigned long		spanned_pages;	/* total size, including holes */
	//内存域中的可用页数
	unsigned long		present_pages;	
	//内存域的关永明,很少用("Normal","DMA","HighMem")
	const char		*name;
} ____cacheline_internodealigned_in_smp;
struct page {
	//描述与体系结构无关的页属性,如此页是否上锁,是否为脏页等
	unsigned long flags;	
	//内存映射相关的一个重要结构,后续会说
	struct address_space *mapping;	
	struct {
		union {
			pgoff_t index;		
			void *freelist;		
		};
		union {
			unsigned counters;
			struct {
				union {
		//atomic_t作为一个结构体其内部只有一个int型变量,
		//将其作为结构体的目的是,为了防止不小心当做int型
		//对象处理了作为结构体,只有定义了特定的函数,才
		//能对其操作表示页表中(虚拟内存中相关概念,后面会
		//详细介绍)有多少项指向该页。
					atomic_t _mapcount;
					struct {
				//最为slab分配器的对象使用,slab的时候
				//无需atomic_t原子操作,这应该反映在数据
				//类型上。
						unsigned inuse:16;
						unsigned objects:15;
						unsigned frozen:1;
					};
				};
		//当前内核中应用该页的次数,如果为0,则代表该Page实例
		//当前不用,可以删除,如果大于0,则该page实例不会被删除
		//注意这里说的删不删除,指的是c中的这个page结构体实例
				atomic_t _count;		
			};
		};
	};
	union {
		//换出页列表,由zone->lru_lock保护,是内存域用来链page的
		struct list_head lru;	
		struct {		
			struct page *next;	
			short int pages;
			short int pobjects;
		};
	};
	union {
		unsigned long private;		
		struct kmem_cache *slab;	
		//内核可以将多个连续的物理页合并为较大的复合页,其中
		//第一个页称为首页,first_page就是指向首页的。
		struct page *first_page;
	};
	//用于高端内存区域中的页,用于存储该页的虚拟地址。
	void *virtual;			
}
  • 在linux中物理内存是以结点->内存域->页帧的方式管理的。物理内存只是一个存储设备,其本身并没有可读/写/执行的属性,也没有地址这一说(或者说,如果物理内存也按照管脚算地址的话,只能是其内部自己yy的地址,我们所说的物理内存地址指的是cpu管脚传出去的物理地址。),只是一个个存储单元,计算机在出厂之前会将cpu和各种内存焊接到主板上,这个时候cpu地址总线上那些管脚传出去一个地址A最终对应到物理内存的某个存储单元上,那么这个存储单元的地址就是A。
  • 先不考虑分页分段机制,在实模式下,cpu访问一个地址,就是通过地址总线传出一个地址,从数据总线接收/发送一个数据,各种物理内存都已经焊好了,非常简单的逻辑。(顺便说下,32位系统下,插上4GB内存,但一般显示可用内存才3.2G左右,这是以为内cpu焊接的时候,其管脚的一部分地址要留给其他外设用,物理内存只是外设之一,所以虽然插上了4GB的内存条,但这些内存无法完全映射到cpu地址总线的地址空间上,不管分不分页也解决不了这个问题,解决方法可见高端内存)。
  • 结点,内存域,页帧的实例,都是c语言中实现的一个个数据结构,由于物理页本身没有任何控制可言,只要地址,数据给到了总线上,就直接访问了,所以内核+cpu需要负责对物理内存的访问控制。结点,内存域,页帧等数据结构是内核控制cpu访问物理内存的最终依据。而物理内存的读写执行控制,应该是cpu根据页表属性执行的???

###分页机制

  • 根据cpu管脚,直接访问物理内存是一种非常简单的方式,但多任务系统中,如果直接使用物理内存,则多个程序之间会相互有影响,比如说程序A当前已经占了某个地址,那么程序B就不能使用该地址了,启动分页的好处在于,在某个进程看来,整个系统的地址空间是自己独占的。
  • 分页机制还有其他的好处,比如根据局部性原理,进程当前需要的数据只占进程的极小一部分,而分页的情况下,可以只给这一小部分分配具体的物理页,而其他部分只是记录在虚拟地址空间中,并不分配具体页。开启分页机制的cpu,在访问虚拟地址时不是通过直接从管脚发送虚拟地址到总线了,而是通过内部的地址转换,转换出真正的物理地址,而这个转换的依据就是页表。
  1. cpu根据一个虚拟地址,查询页表。
  2. 如果页表中显示已经映射了物理页,则获取物理地址,发送到总线;如果页表中显示没映射物理页,则cpu发出缺页中断。
    在没有分页机制的时候,cpu直接访问物理内存,没有缺页一说,所以程序的所有数据必须全部写入到物理内存中才可以访问。分段就不说了,貌似目前很少有用的了。
  • 物理内存是一片连续的存储空间,本身并没有单位/页之分,而为了便于管理,在分页的时候,将虚拟地址/物理地址空间都划分为一些等长的区域,这样的区域被称为页。无论是物理内存还是虚拟地址空间的内存分配,都是以页为最小粒度进行操作的。

  • 物理内存中的一页被称为物理页/页帧/页框。虚拟地址空间和物理内存间的关系简单来说就是一张地址与地址对应的表,这张表完全可以如下设计(这不是真的页表,为了说明问题,举个例子):
    | 虚拟地址 | 物理地址 |
    | ------ | — |
    | 0x00000000 | 0xXXXX0XXX|
    | 0x00001000 | 0xXXXX1XXX|
    | 0x00002000 | 0xXXXX2XXX|
    | … | …|
    | 0x00010000 | 空|
    | 0x00020000 | 空|
    | 0x00030000 | 空|
    | … | …|
    | 0xFFFFEFFF | 0xXXXXXXXX|
    | 0xFFFFFFFF | 0xXXXXXXXX|

  • 为啥说是以页为单位的,如果是以地址为单位的,那么上面这张表的真实大小至少4G(地址)* 8Byte(每一行占的最小空间),整个cpu寻址空间都不够存的。

  • 从上表可以看出,为什么说分页可以节省物理内存,因为一些我们没用到的虚拟地址,如上面的0x00010000就可以不对应物理地址,知道需要的时候再映射或缺页中断填上,而真正的物理内存是连续的,即使某一段没有用到,也必须放在那里。

  • 以上这样的数据结构,就叫做页表,如果要使用分页机制,在开启分页机制之前,要先弄好这么一张表,具体地址如何对应,就看设计者的心情了。这样一张表直接扔在物理内存中,然后把物理内存中这个表的基地址告诉cpu,通常是扔到cpu的某个控制寄存器中(如cr3),这个寄存器也不是随便选的,具体扔到哪个寄存器中好使,得查cpu手册,看看人家是怎么提供分页机制的。

  • 此时开启分页机制,之后cpu指令集中任何一条指令,只要与地址有关,那么这个地址(A)都会被cpu重新解析,cpu知道cr3寄存器中记的是页表的物理地址,会直接将这个地址发送到地址总线去查询页表中的内容,然后根据cr3中基地址+页偏移,就找到了这个虚拟地址页-物理地址页的关系,就相当于找到了虚拟地址A所对应的物理地址B,然后再通过地址总线去访问地址B的内容并返回。

  • 以上的页表存在一个问题,就是太大了…,首先我们知道了为啥得以页为单位,要不然光一张表就比整个cpu地址总线空间都大了。以下的计算假设在32位cpu上,页大小为4KB 。来算一下,32cpu地址总线32位,地址空间 0-4GB,以4KB为单位,那么一共需要 2^32 / 2^12 = 220。然后每一个表项至少得存两个地址,一个虚拟地址一个物理地址(由于物理内存也按照页为单位了,实际上我们在页表数组中表示一个页,只需要20个bit即可了,后面12位都是0,不用算了,但怎么说访问也应该对其,所以还的32bit一个地址,算起来就得8B一个表项)所以说,**整个页表就得220 * 8 = 2^23**。整个内核地址空间才1GB = 2^30,这个页表是一个进程一个的,一个进程就占8MB页表项,没法愉快的玩耍了…。所以,得想办法减少页表站的空间

  • 前面说了分页的好处,弄出了一个虚拟地址空间,随便用,但问题是虚拟地址空间需要页表,而一个页表数组占得空间有太大了,怎么办?多级分页

  • 为啥多级分页能减小内存空间呢,比如说前面的那个例子改一下,一个进程的内存分布如下:

    虚拟地址物理地址
    0x000000000xXXXX0XXX
    0x000010000xXXXX1XXX
    0x000020000xXXXX2XXX
    0x00010000
    0x00020000
    0x00030000
    0xFFFFEFFF0xXXXXXXXX
    0xFFFFFFFF0xXXXXXXXX

    那么用数组的形式组织页表,必然需要2^23次方大小的空间,因为这是数组,哪怕中间都是空,你也得把地方站住。而如果用二级页表来实现,只需要一张顶级页表,两张二级页表即可。因为对于二级页表来说,顶级页表中某一项为空,就可以不分配对应的二级页表,这个空间自然就生下来了。当然如果4GB全部需要映射,分级页表肯定比数组占的空间要多一点,但问题是…呵呵 还真没见过4GB被完全映射的。

  • 物理内存一页的大小,分页机制需要几级页表,这个是与具体的体系结构,cpu型号有关的,最新的linux将页表分为4级别,一般cpu的分页机制都少于4级,这样对于<4级的cpu,如3级的cpu,linux只需定义其中某一级页表站位为0即可。


##物理内存的分配

  • 物理内存的分配指的是,当进程需要真正将虚拟地址和物理地址绑定的时候,如何选择使用绑定哪个物理地址,整个物理内存最终都是对应到一个个内核的page结构来管理的,在page中记录着物理页的已分配/空闲状态,内核通过伙伴系统/slab分配器来分配具体的物理页。
  • 物理内存对于cpu来说,是任何时候,任何地址都可以被访问到的(开启分页就自己构造页表,反正不管怎样,内核代码一定是可以访问到所有物理内存地址的),这里的管理不是说控制那块地址能不能访问,而是记录与管理哪块地址我已经分配给了谁去访问**。再强调一下,缺页中断只是虚拟地址的事情,真正的物理地址不存在无法访问这一说。**
  • 现在需要申请一段物理页 ,这时就需要调用系统的物理内存分配算法,来确定当前应该使用哪段物理内存。
  • 伙伴算法的思想是:将系统中的空闲内存块两两分组,魅族中的两个内存块称作伙伴,伙伴的分配是独立的,但若两个伙伴都是空闲的,内核会将其合并为一个更大的内存块。内核对 所有大小相同的伙伴都放置到一个列表中管理,分配的最小单位为页
  • slab缓存,用于分配比完整页帧更小的内存块,slab缓存会自动维护与伙伴系统的交互,这里后续再做介绍。

##页表

  • linux内核总是假设cpu使用四级页表,页表管理分为两部分:体系结构相关/体系结构无关部分。但所有数据结构和相关函数几乎都定义在特定于体系结构的文件中。一般是arch/page.h和arch/pgtable.h,这里以arm体系结构中两级页表为例。
  • 内核假定void*和unsigned long类型之间可以相互转换而不损失信息,在linux所支持的所有体系结构上,这一结论都是正确的。内存管理中更喜欢用unsigned long ,因为其更容易操作。
  • 按照linux系统四级页表的结构,虚拟地址可以被分为5个部分:
|--------------------虚拟地址-------------------|
|PGD|PUD        |PMD      |PTE      |Offset    |
                                    |PAGE_SHIFT|
                          |<----PMD_SHIFT----->|
                |<---------PUD_SHIFT --------->|
    |<----------------PGDIR_SHIFT------------->|
|<--------------BITS_PER_LONG----------------->|

//其中BITS_PER_LONG记录的是一个long变量占的bit数,因而也适用于指向虚拟地址空间的通用指针。
//arch/arm/include/asm/types.h
#define BITS_PER_BYTE		8
#define BITS_PER_LONG (sizeof(long) * BITS_PER_BYTE)
//kernel/arch/arm/include/page.h
//PAGE_SHIFT是页内偏移,用于确定页帧内部具体访问哪个地址
#define PAGE_SHIFT		12
//kernel/arch/arm/include/asm/pgtable-2level.h
#define PMD_SHIFT		21
#define PGDIR_SHIFT		21
//在arm中就没有定义PUD_SHIFT,这个定义在:
//kernel/include/asm-generic/Pgtable-nopud.h
#define PMD_SHIFT	PUD_SHIFT

  • 可见,在二级页表的arm体系结构中,第一级页表是PGD,展位
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值