Linux内存管理(2):内存描述

    linux内存管理建立在基本的分页机制基础上,在linux内核中RAM的某些部分将会永久的分配给内核,并用来存放内核代码以及静态内核数据结构。RAM的其余部分称为动态内存,这不仅是进程所需的宝贵资源,也是内核本身所需的宝贵资源。实际上,整个系统的性能取决于如何有效地管理动态内存。因此,现在所有多任务操作系统都在经历优化对动态内存的使用,也就是说,尽可能做到当要时分配,不需要时释放。
    内存管理是os中最复杂的管理机制之一。linux中采用了很多有效的管理方法,包括页表管理、高端内存(临时映射区、固定映射区、永久映射区、非连续内存区)管理、为减小外部碎片的伙伴系统、为减小内部碎片的slab机制、伙伴系统未建立之前的页面分配制度以及紧急内存管理等等。

    linux使用于广泛的体系结构,因此需要用一种与体系结构无关的方式来描述内存。linux用VM描述和管理内存。在VM中使用的普遍概念就是非一致内存访问。对于大型机器而言,内存会分成许多簇,依据簇与处理器“距离”的不同,访问不同的簇会有不同的代价。每个簇都被认为是一个节点(pg_data_t),每个节点被分成很多的称为管理区(zone)的块,用于表示内存中的某个范围。zone的类型除了ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM以外,从linux2.6.32开始引入了ZONE_MOVABLE,用于适应大块连续内存的分配。每个物理页面由一个page结构体描述,所有的页结构都存储在一个全局的mem_map数组中(非平板模式),该数组通常存放在ZONE_NORMAL内存区域的首部,或者就在内存系统中为装入内核映像而预留的区域之后。内存描述的层次结构为pg_data_t--->zone--->mem_map数组(ZONE_XXX类型)--->page,如下图。下面的以2.6.32.45的内核代码为参考来介绍。

图1 内存描述的层次结构

    1、节点:pg_data_t

    内存的每个节点都有pg_data_t描述,在分配一个页面时,linux采用节点局部分配的策略,从最靠近运行中的CPU的节点分配内存。由于进程往往是在同一个CPU上运行,因此从当前节点得到的内存很可能被用到。pg_data_t在include/linux/mmzone.h中,如下:

/*
 * pg_data_t结构用在带有CONFIG_DISCONTIGMEM编译选项的机器中(最新的NUMA机器),
 * 以表示比zone结构更高一层次的内存区域。
 * 在NUMA机器上,每个NUMA节点由一个pg_data_t来描述它的内存布局。内存使用统计和
 * 页面交换数据结构由每个zone区域来维护
 */
struct bootmem_data;
typedef struct pglist_data {
    /* 该节点内的内存区。可能的区域类型用zone_type表示 */
	struct zone node_zones[MAX_NR_ZONES];
    /* 该节点的备用内存区。当节点没有可用内存时,就从备用区中分配内存 */
	struct zonelist node_zonelists[MAX_ZONELISTS];
    /* 可用内存区数目,即node_zones数据中保存的最后一个有效区域的索引 */
	int nr_zones;
#ifdef CONFIG_FLAT_NODE_MEM_MAP	/* means !SPARSEMEM */
    /* 在平坦型的内存模型中,它指向本节点第一个页面的描述符 */
	struct page *node_mem_map;
#ifdef CONFIG_CGROUP_MEM_RES_CTLR
    /* cgroup相关 */
	struct page_cgroup *node_page_cgroup;
#endif
#endif
    /* 在内存子系统初始化以前,即boot阶段也需要进行内存管理。 
     * 此结构用于这个阶段的内存管理。 
     */
	struct bootmem_data *bdata;
#ifdef CONFIG_MEMORY_HOTPLUG
    /* 当系统支持内存热插拨时,这个锁用于保护本结构中的与节点大小相关的字段。
     * 当你希望node_start_pfn,node_present_pages,node_spanned_pages仍保持常量时,
     * 需要持有该锁。
     */
	spinlock_t node_size_lock;
#endif
	unsigned long node_start_pfn; /*起始页面帧号,指出该节点在全局mem_map中的偏移*/
	unsigned long node_present_pages; /* 物理页的总数 */
	unsigned long node_spanned_pages; /* 物理页范围的跨度,包括holes */
	int node_id;  /* 节点编号 */
    /* 等待该节点内的交换守护进程的等待队列。将节点中的页帧换出时会用到 */
	wait_queue_head_t kswapd_wait;
    /* 负责该节点的交换守护进程 */
	struct task_struct *kswapd;
    /* 由页交换子系统使用,定义要释放的区域大小 */
	int kswapd_max_order;
} pg_data_t;
    该结构的主要数据有内存区、备用内存区、可用内存区计数、锁、物理页总数、物理页范围跨度、所属交换守护进程等。一个节点通过node_zones数组有维护多个zone管理区。
     2、管理区:zone
    管理区用于跟踪诸如页面使用情况统计数,空闲区域信息和锁信息等。每个管理区由一个zone结构体描述,管理区的类型由zone_type描述,都在include/linux/mmzone.h中。如下:
enum zone_type {
#ifdef CONFIG_ZONE_DMA
	/*
	 * ZONE_DMA is used when there are devices that are not able
	 * to do DMA to all of addressable memory (ZONE_NORMAL). Then we
	 * carve out the portion of memory that is needed for these devices.
	 * The range is arch specific.
	 *
	 * Some examples
	 *
	 * Architecture		Limit
	 * ---------------------------
	 * parisc, ia64, sparc	<4G
	 * s390			<2G
	 * arm			Various
	 * alpha		Unlimited or 0-16MB.
	 *
	 * i386, x86_64 and multiple other arches
	 * 			<16M.
	 */
	ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
	/*
	 * x86_64 needs two ZONE_DMAs because it supports devices that are
	 * only able to do DMA to the lower 16M but also 32 bit devices that
	 * can only do DMA areas below 4G.
	 */
	ZONE_DMA32,
#endif
	/*
	 * Normal addressable memory is in ZONE_NORMAL. DMA operations can be
	 * performed on pages in ZONE_NORMAL if the DMA devices support
	 * transfers to all addressable memory.
	 */
	ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
	/*
	 * A memory area that is only addressable by the kernel through
	 * mapping portions into its own address space. This is for example
	 * used by i386 to allow the kernel to address the memory beyond
	 * 900MB. The kernel will set up special mappings (page
	 * table entries on i386) for each page that the kernel needs to
	 * access.
	 */
	ZONE_HIGHMEM,
#endif
	ZONE_MOVABLE,
	__MAX_NR_ZONES
};
    管理区类型介绍:
    (1)ZONE_DMA:用在当有设备不能通过DMA访问整个可寻址内存(ZONE_NORMAL)的情况下。这时我们就要为这些设备专门开辟出一段内存,通常是低端内存区域。ZONE_DMA的内存范围与体系结构有关,parisc、ia64以及sparc中是小于4G;s390是小于2G;arm中是可变的多种多样的;alpha中是无限或者0-16MB;i386、x86_64以及其他很多体系结构是小于16MB(0-16MB)。
    (2)ZONE_DMA32:注意x86_64需要两个ZONE_DMA区域,因为它既支持只能访问16MB以下DMA区域的设备,也支持只能访问4GB以下DMA区域的32位设备,ZONE_DMA32针对后一种情况。
    (3)ZONE_NORMAL:正常的可访问内存。如果DMA设备能支持传输数据到整个可访问内存,则DMA操作也能在ZONE_NORMAL类型的页面上进行。
    (4)ZONE_HIGHMEM:映射到内核代码本身的内核地址空间,一般是高端内存区域,它只能由内核访问,用户空间访问不到。所有的内核操作都只能使用这个内存区域来进行,因此这是对性能至关重要的区域。例如i386允许内核访问超过900MB的内存,对每个内核需要访问的页面,内核将设置特别的映射项(i386上的页表项)。
    (5)ZONE_MOVABLE:这是一个伪内存段。为了防止形成物理内存碎片,可以将虚拟地址对应的物理地址进行迁移,使多个碎片合并成一块连续的大内存。ZONE_MOVABLE类型用于适应大块连续内存的分配。
struct zone {
	/* 被页面分配器访问的通用域 */

	/* 本管理区的三个水线值:高水线(比较充足)、低水线、MIN水线。会被*_wmark_pages(zone)宏访问 */
	unsigned long watermark[NR_WMARK];

	/* 当可用页数在本水线值以下时,在读取可用页计数值时,需要增加额外的工作以避免每个CPU的计数器
	 * 漂移导致水线值被打破	
	 */
	unsigned long percpu_drift_mark;

	/* 我们不知道即将分配的内存是否可用,以及最终是否会被释放,因此为了避免浪费几GB的RAM,我们
	 * 必须额外保留一些低端区域的内存(如DMA区域)供驱动使用。否则我们会面临在低端区域内出现
	 * OOM(Out of Memory)的风险,尽管这时高端区域还有大量可用的RAM。本字段是指从上级内存区
	 * 退到回内存区时,需要额外保留的内存数量。如果在运行时sysctl_lowmem_reserve_ratio控制
	 * 改变,它会被重新计算
	 */
	unsigned long		lowmem_reserve[MAX_NR_ZONES];

#ifdef CONFIG_NUMA
	int node; /* 所属的NUMA节点 */
	/* 未映射的页(即可回收的页)超过此值,将进行页面回收 */
	unsigned long		min_unmapped_pages;
	/* 管理区中用于slab的可回收页大于此值时,将回收slab中的缓存页 */ 
	unsigned long		min_slab_pages;
	 /* 
      * 每CPU的页面缓存。 
      * 当分配单个页面时,首先从该缓存中分配页面。这样可以: 
      * 避免使用全局的锁 
      * 避免同一个页面反复被不同的CPU分配,引起缓存页的失效。 
      * 避免将管理区中的大块分割成碎片。 
      */  
	struct per_cpu_pageset	*pageset[NR_CPUS];
#else
	struct per_cpu_pageset	pageset[NR_CPUS];
#endif
	/* 该锁用于保护伙伴系统数据结构。即保护free_area相关数据 */ 
	spinlock_t		lock;
#ifdef CONFIG_MEMORY_HOTPLUG
	/* 用于保护spanned/present_pages等变量。这些变量几乎不会发生变化,除非发生了内存热插拨操作。 
     * 这几个变量并不被lock字段保护。并且主要用于读,因此使用读写锁 */
	seqlock_t		span_seqlock;
#endif
	/* 伙伴系统的主要变量。这个数组定义了11个队列,每个队列中的元素都是大小为2^n的页面 */
	struct free_area	free_area[MAX_ORDER];

#ifndef CONFIG_SPARSEMEM
	/* 本管理区里的pageblock_nr_pages块标志数组,参考pageblock-flags.h
	 * 在SPARSEMEM中,本映射存储在结构mem_section中 */
	unsigned long		*pageblock_flags;
#endif /* CONFIG_SPARSEMEM */

	/* 填充的未用字段,确保后面的字段是缓存行对齐的 */ 
	ZONE_PADDING(_pad1_)

	/* 被页面回收扫描器访问的通用域 */
	/* 
    * lru相关的字段用于内存回收。这个锁用于保护这几个回收相关的字段。 
    * lru用于确定哪些字段是活跃的,哪些不是活跃的,并据此确定应当被写回到磁盘以释放内存。 
     */  
	spinlock_t		lru_lock;
	/* 匿名活动页、匿名不活动页、文件活动页、文件不活动页链表头 */
	struct zone_lru {
		struct list_head list;
	} lru[NR_LRU_LISTS];

	struct zone_reclaim_stat reclaim_stat; /* 页面回收状态 */
	/* 自从最后一次回收页面以来,扫过的页面数 */
	unsigned long		pages_scanned;
	unsigned long		flags;		   /* 管理区标志,参考下面 */

	/* Zone statistics */
	atomic_long_t		vm_stat[NR_VM_ZONE_STAT_ITEMS];

	/*
	 * prev_priority holds the scanning priority for this zone.  It is
	 * defined as the scanning priority at which we achieved our reclaim
	 * target at the previous try_to_free_pages() or balance_pgdat()
	 * invokation.
	 *
	 * We use prev_priority as a measure of how much stress page reclaim is
	 * under - it drives the swappiness decision: whether to unmap mapped
	 * pages.
	 *
	 * Access to both this field is quite racy even on uniprocessor.  But
	 * it is expected to average out OK.
	 */
	int prev_priority;

	/*
	 * The target ratio of ACTIVE_ANON to INACTIVE_ANON pages on
	 * this zone's LRU.  Maintained by the pageout code.
	 */
	unsigned int inactive_ratio;

	/* 为cache对齐 */
	ZONE_PADDING(_pad2_)
	/* Rarely used or read-mostly fields */

	/*
	 * wait_table		-- the array holding the hash table
	 * wait_table_hash_nr_entries	-- the size of the hash table array
	 * wait_table_bits	-- wait_table_size == (1 << wait_table_bits)
	 *
	 * The purpose of all these is to keep track of the people
	 * waiting for a page to become available and make them
	 * runnable again when possible. The trouble is that this
	 * consumes a lot of space, especially when so few things
	 * wait on pages at a given time. So instead of using
	 * per-page waitqueues, we use a waitqueue hash table.
	 *
	 * The bucket discipline is to sleep on the same queue when
	 * colliding and wake all in that wait queue when removing.
	 * When something wakes, it must check to be sure its page is
	 * truly available, a la thundering herd. The cost of a
	 * collision is great, but given the expected load of the
	 * table, they should be so rare as to be outweighed by the
	 * benefits from the saved space.
	 *
	 * __wait_on_page_locked() and unlock_page() in mm/filemap.c, are the
	 * primary users of these fields, and in mm/page_alloc.c
	 * free_area_init_core() performs the initialization of them.
	 */
	wait_queue_head_t	* wait_table;
	unsigned long		wait_table_hash_nr_entries;
	unsigned long		wait_table_bits;

	/*
	 * Discontig memory support fields.
	 */
	struct pglist_data	*zone_pgdat; /* 本管理区所属的节点 */
	/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
	unsigned long		zone_start_pfn; /* 管理区的页面在mem_map中的偏移 */

	/*
	 * zone_start_pfn, spanned_pages and present_pages are all
	 * protected by span_seqlock.  It is a seqlock because it has
	 * to be read outside of zone->lock, and it is done in the main
	 * allocator path.  But, it is written quite infrequently.
	 *
	 * The lock is declared along with zone->lock because it is
	 * frequently read in proximity to zone->lock.  It's good to
	 * give them a chance of being in the same cacheline.
	 */
	unsigned long		spanned_pages;	/* total size, including holes */
	unsigned long		present_pages;	/* amount of memory (excluding holes) */

	const char		*name; /* 很少使用的域 */
} ____cacheline_internodealigned_in_smp;
    zone结构中的字段主要分两大类,一类是被页面分配器访问的字段,有水线值、保留的DMA内存区域数量、所属NUMA节点、未映射页数、slab中缓存页数、每个CPU的缓存页面集、伙伴系统可用区域数组free_area、页面标志数组等。一类是被页面回收器访问的字段,有LRU链表(用于LRU页面回收算法)、页面回收统计信息、所属的pglist_data节点、页面在mem_map中的偏移等。
     3、物理页面:page
    系统中每个物理页面都有一个相关联的page用于记录该页面的状态。在include/linux/mm_types.h中,如下:
/*
 * 系统中每个物理页面有一个相关联的page结构,用于记录该页面的状态。注意虽然当该页面是
 * 一个缓存页时,rmap结构能告诉我们谁正在映射它,但我们并没有一般的方法来跟踪哪个进程正在使用该页面
 */
struct page {
	unsigned long flags;		/* 原子标志,一些可以会被异步更新 */
	atomic_t _count;		/* 使用计数,参考下面 */
	union {
		atomic_t _mapcount;	/* 在mms中映射的ptes计数,用于表明页面什么时候被映射,
					  * 并且限制反向映射搜索
					  */
		struct {		/* SLUB */
			u16 inuse;
			u16 objects;
		};
	};
	union {
	    struct {
		unsigned long private;		/* 映射时的私有非透明数据:
					 	 * 如果设置PagePrivate,则用作buffer_heads;
						 * 如果设置PageSwapCache,则用作swp_entry_t;
						 * 如果设置PG_buddy,则表示在伙伴系统中的顺序编号
						 */
		struct address_space *mapping;	/* 如果低端bit清除,则指向inode地址空间,或者为null.
						 * 如果页面被映射为匿名内存,低端bit设置,则指向
						 * anon_vma对象,参看PAGE_MAPPING_ANON
						 */
	    };
#if USE_SPLIT_PTLOCKS
	    spinlock_t ptl;
#endif
	    struct kmem_cache *slab;	/* SLUB: 指向slab的指针 */
		/* 如果属于伙伴系统,并且不是伙伴系统中的第一个页则指向第一个页 */
	    struct page *first_page;
	};
	union {  /* 如果是文件映射,那么表示本页面在文件中的位置(偏移) */
		pgoff_t index;		/* Our offset within mapping. */
		void *freelist;		/* SLUB: freelist req. slab lock */
	};
	struct list_head lru;		/* Pageout list, eg. active_list
					 * protected by zone->lru_lock !
					 */
	/*
	 * On machines where all RAM is mapped into kernel address space,
	 * we can simply calculate the virtual address. On machines with
	 * highmem some memory is mapped into kernel virtual memory
	 * dynamically, so we need a place to store that address.
	 * Note that this field could be 16 bits on x86 ... ;)
	 *
	 * Architectures with slow multiplication can define
	 * WANT_PAGE_VIRTUAL in asm/page.h
	 */
#if defined(WANT_PAGE_VIRTUAL)
	void *virtual;			/* 内核虚拟地址(如果没有被内核映射,则为NULL,例如高端内存hignmem) */
#endif /* WANT_PAGE_VIRTUAL */
#ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS
	unsigned long debug_flags;	/* Use atomic bitops on this */
#endif

#ifdef CONFIG_KMEMCHECK
	/* kmemcheck想跟踪一个page中的每个byte的状态,这是一个指向这种状态块的指针。
	 * 如果没有被跟踪,则为NULL
	 */
	void *shadow;
#endif
};
    该结构主要包含原子标志、使用计数、指向的地址空间、指向slab的指针、文件中的位置(如果是文件映射)、状态跟踪指针等。
     4、全局的mem_map数组:定义在include/linux/mmzone.h中,如下:
#ifndef CONFIG_DISCONTIGMEM
/* 物理页数组,对discontigmem使用pgdat->lmem_map */
extern struct page *mem_map;
#endif
    这个数组保存了所有的物理页page结构,它存储在ZONE_NORMAL内存区域的开头,用于跟踪所有的物理页面。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值