Linux内存管理
1.页
Linux的内核把物理页作为内存管理的基本单位。而视窗操作系统中的基本单位是进程和线程,与Linux的不同。
内存管理单元(MMU):管理内存并把虚拟地址转换为物理地址的硬件。
MMU通常以页为单位来管理系统中的页表。当然,页的大小在不同的体系结构下是不同的0.32位一般支持4KB的页,而64位支持8KB的页。
Linux内核中用struct page结构体类型表示系统中的物理页,该结构位于<linux \ include \ Mm.h>中,
struct page {
/**
* 一组标志,也对页框所在的管理区进行编号
* 在不支持NUMA的机器上,flags中字段中管理索引占两位,节点索引占一位。
* 在支持NUMA的32位机器上,flags中管理索引占用两位。节点数目占6位。
* 在支持NUMA的64位机器上,64位的flags字段中,管理区索引占用两位,节点数目占用10位。
*/
page_flags_t flags; /* Atomic flags, some possibly
* updated asynchronously */
/**
* 页框的引用计数。当小于0表示没有人使用。
* Page_count返回_count+1表示正在使用的人数。
*/
atomic_t _count; /* Usage count, see below. */
/**
* 页框中的页表项数目(没有则为-1)
* -1: 表示没有页表项引用该页框。
* 0: 表明页是非共享的。
* >0: 表示而是共享共享的。
*/
atomic_t _mapcount; /* Count of ptes mapped in mms,
* to show when page is mapped
* & limit reverse map searches.
*/
/**
* 可用于正在使用页的内核成分(如在缓冲页的情况下,它是一个缓冲器头指针。)
* 如果页是空闲的,则该字段由伙伴系统使用。
* 当用于伙伴系统时,如果该页是一个2^k的空闲页块的第一个页,那么它的值就是k.
* 这样,伙伴系统可以查找相邻的伙伴,以确定是否可以将空闲块合并成2^(k+1)大小的空闲块。
*/
unsigned long private; /* Mapping-private opaque data:
* usually used for buffer_heads
* if PagePrivate set; used for
* swp_entry_t if PageSwapCache
* When page is free, this indicates
* order in the buddy system.
*/
/**
* 当页被插入页高速缓存时使用或者当页属于匿名页时使用)。
* 如果mapping字段为空,则该页属于交换高速缓存。
* 如果mapping字段不为空,且最低位为1,表示该页为匿名页。同时该字段中存放的是指向anon_vma描述符的指针。
* 如果mapping字段不为空,且最低位为0,表示该页为映射页。同时该字段指向对应文件的address_space对象。
*/
struct address_space *mapping; /* If low bit clear, points to
* inode address_space, or NULL.
* If page mapped as anonymous
* memory, low bit is set, and
* it points to anon_vma object:
* see PAGE_MAPPING_ANON below.
*/
/**
* 作为不同的含义被几种内核成分使用。
* 在页磁盘映象或匿名区中表示存放在页框中的数据的位置。
* 或者它存放在一个换出页标志符。
*/
pgoff_t index; /* Our offset within mapping. */
/**
* 包含页的最近最少使用的双向链表的指针。
*/
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; /* Kernel virtual address (NULL if
not kmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */
};
· flag域:用于存放页的状态,包括页是不是脏的,是不被被锁定在内存中等。每一位单独表示一种状态,可表示32种不同状态。
· _count域存放页面的引用计数。当_count为-1时,说明当前内核并没有引用这一页。通过调用page_count()函数检查该域名。
·虚拟域是页的虚拟地址,也就是页在虚拟内存中的地址。·高端内存并不永久地映射到内核地址空间上,这时虚拟域的值为NULL。
2.区
一些硬件由于存在缺陷而引起的内存寻址问题:
·一些硬件只能用某些特定的内存地址来执行DMA(直接内存访问)。
·一些体系结构的内存的物理寻址范围比虚拟寻址范围大,就会有一些内存不能永久地映射到内核空间上。
Linux的的为解决这些问题,使用了如下四种区:
·ZONE_DMA - 这个区包含的页用来执行DMA操作(进行直接内存访问)。物理内存范围为<16MB。
·ZONE_DMA32 - 和ZONE_DMA类似,该区包含的页面可执行DMA操作;不同之处在于这些页面只能被32位设备访问,比ZONE_DMA更大。
·ZONE_NORMAL - 能正常映射的页,即正常柯林斯寻址的页物理内存范围的英文。16〜896MB。
·ZONE_HIGHMEM -这个区包含“ 高端内存 “,动态映射的页。 linux32镜像镜像位下映射高于1G的内存,在64位上没有该区。物理内存范围大于896MB。
Linux的的把系统的页划分为区,形成不同的内存池,根据用途进行分配。但是,分配不能跨区界限的。比如,尽管用于DMA的内存必须从DMA中进行分配,但是一般用途的内存既能从ZONE_DMA中分配,也能从ZONE_NORMAL分配。
Inter x86-64体系结构可以映射和处理64位的内存空间,所以X86-64没有ZONE-HIGHMEM区,所有物理内存都处于ZONE_NORMAL和ZONE_DMA中。
/**
* 包含低16MB的内存页框。
*/
#define ZONE_DMA 0
/**
* 包含高于16MB且低于896MB的内存页框。
*/
#define ZONE_NORMAL 1
/**
* 包含从896MB开始高于896MB的内存页框。
*/
#define ZONE_HIGHMEM 2
管理内存区描述符结构区,
struct zone {
/* Fields commonly accessed by the page allocator */
/**
* 管理区中空闲页的数目
*/
unsigned long free_pages;
/**
* Pages_min-管理区中保留页的数目
* Page_low-回收页框使用的下界。同时也被管理区分配器为作为阈值使用。
* pages_high-回收页框使用的上界,同时也被管理区分配器作为阈值使用。
*/
unsigned long pages_min, pages_low, pages_high;
/**
* 为内存不足保留的页框
*/
unsigned long lowmem_reserve[MAX_NR_ZONES];
/**
* 用于实现单一页框的特殊高速缓存。
* 每CPU、每内存管理区都有一个。包含热高速缓存和冷高速缓存。
*/
struct per_cpu_pageset pageset[NR_CPUS];
/*
* free areas of different sizes
*/
/**
* 保护该描述符的自旋锁
*/
spinlock_t lock;
struct free_area free_area[MAX_ORDER];
ZONE_PADDING(_pad1_)
/* Fields commonly accessed by the page reclaim scanner */
/**
* 活动以及非活动链表使用的自旋锁。
*/
spinlock_t lru_lock;
/**
* 管理区中的活动页链表
*/
struct list_head active_list;
/**
* 管理区中的非活动页链表。
*/
struct list_head inactive_list;
/**
* 回收内存时需要扫描的活动页数。
*/
unsigned long nr_scan_active;
/**
* 回收内存时需要扫描的非活动页数目
*/
unsigned long nr_scan_inactive;
/**
* 管理区的活动链表上的页数目。
*/
unsigned long nr_active;
/**
* 管理区的非活动链表上的页数目。
*/
unsigned long nr_inactive;
/**
* 管理区内回收页框时使用的计数器。
*/
unsigned long pages_scanned; /* since last reclaim */
/**
* 在管理区中填满不可回收页时此标志被置位
*/
int all_unreclaimable; /* All pages pinned */
/**
* 临时管理区的优先级。
*/
int temp_priority;
/**
* 管理区优先级,范围在12和0之间。
*/
int prev_priority;
ZONE_PADDING(_pad2_)
/**
* 进程等待队列的散列表。这些进程正在等待管理区中的某页。
*/
wait_queue_head_t * wait_table;
/**
* 等待队列散列表的大小。
*/
unsigned long wait_table_size;
/**
* 等待队列散列表数组的大小。值为2^order
*/
unsigned long wait_table_bits;
/*
* Discontig memory support fields.
*/
/**
* 内存节点。
*/
struct pglist_data *zone_pgdat;
/**
* 指向管理区的第一个页描述符的指针。这个指针是数组mem_map的一个元素。
*/
struct page *zone_mem_map;
/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
/**
* 管理区的第一个页框的下标。
*/
unsigned long zone_start_pfn;
/**
* 以页为单位的管理区的总大小,包含空洞。
*/
unsigned long spanned_pages; /* total size, including holes */
/**
* 以页为单位的管理区的总大小,不包含空洞。
*/
unsigned long present_pages; /* amount of memory (excluding holes) */
/*
* rarely used fields:
*/
/**
* 指针指向管理区的传统名称:DMA、NORMAL、HighMem
*/
char *name;
} ____cacheline_maxaligned_in_smp;
其中,锁定域是一个自旋锁,防止该结构被并发访问。(该域只保护结构,不保护驻留在这个区的所有页)。而且没有特定的锁保护单个页。
3.获得页
alloc_pages()
请求分配一组连续页框,这是管理区分配器的核心。
该函数分配2 ^ order个连续的物理页,并返回一个指针,该指针指向第一个页面的结构体;如果出错返回NULL。
而函数alloc_page()只分配一页,返回指向页结构的指针。
· gfp_mask:在内存分配请求中指定的标志。
· order:连续分配的页框数量的对数(2 ^ order个)
· zonelist:zonelist数据结构的指针。该结构按优先次序描述了适用于内存分配的内存管理区
/**
* 分配2^order个连续的页框。它返回第一个所分配页框描述符的地址或者返回NULL
*/
static inline struct page *
alloc_pages(unsigned int gfp_mask, unsigned int order)
{
if (unlikely(order >= MAX_ORDER))
return NULL;
return alloc_pages_current(gfp_mask, order);
}
在alloc_pages_current()中调用__alloc_pages(),
struct page * fastcall
__alloc_pages(unsigned int gfp_mask, unsigned int order,
struct zonelist *zonelist)
{
const int wait = gfp_mask & __GFP_WAIT;
struct zone **zones, *z;
struct page *page;
struct reclaim_state reclaim_state;
struct task_struct *p = current;
int i;
int classzone_idx;
int do_retry;
int can_try_harder;
int did_some_progress;
might_sleep_if(wait);
can_try_harder = (unlikely(rt_task(p)) && !in_interrupt()) || !wait;
zones = zonelist->zones; /* the list of zones suitable for gfp_mask */
if (unlikely(zones[0] == NULL)) {
/* Should this ever happen?? */
return NULL;
}
classzone_idx = zone_idx(zones[0]);
restart:
/* Go through the zonelist once, looking for a zone with enough free */
/**
* 扫描包含在zonelist数据结构中的每个内存管理区
*/
for (i = 0; (z = zones[i]) != NULL; i++) {
执行对内存管理区的第二次扫描,将值Z-> pages_min作为阀值传入。
for (i = 0; (z = zones[i]) != NULL; i++)
wakeup_kswapd(z, order);
/*
* Go through the zonelist again. Let __GFP_HIGH and allocations
* coming from realtime tasks to go deeper into reserves
*/
/**
* 执行对内存管理区的第二次扫描,将值z->pages_min作为阀值传入。这个值已经在上一步的基础上降低了。
* 当然,实际的min值还是要由can_try_harder和gfp_high确定。z->pages_min仅仅是一个参考值而已。
*/
for (i = 0; (z = zones[i]) != NULL; i++) {
if (!zone_watermark_ok(z, order, z->pages_min,
classzone_idx, can_try_harder,
gfp_mask & __GFP_HIGH))
continue;
page = buffered_rmqueue(z, order, gfp_mask);
if (page)
goto got_pg;
}
如果产生内存分配的内核控制路径不是一个中断处理程序或者可延迟函数,并且它试图回收页框(PF_MEMALLOC,TIF_MEMDIE标志被置位),那么才对内存管理区进行第三次扫描。
if (((p->flags & PF_MEMALLOC) || unlikely(test_thread_flag(TIF_MEMDIE))) && !in_interrupt()) {
/* go through the zonelist yet again, ignoring mins */
for (i = 0; (z = zones[i]) != NULL; i++) {
/**
* 本次扫描就不调用zone_watermark_ok,它忽略阀值,这样才能从预留的页中分配页。
* 允许这样做,因为是这个进程想要归还页框,那就暂借一点给它吧(呵呵,舍不得孩子套不到狼)。
*/
page = buffered_rmqueue(z, order, gfp_mask);
if (page)
goto got_pg;
}
/**
* 不论是高端内存区还是普通内存区、还是DMA内存区,甚至这些管理区中保留的内存都没有了。
*/
goto nopage;
}
后面代码省略。
page_address()
把给定的页转换成对应的逻辑地址,也就是线性地址。(/Mm.h)
/**
* page_address返回页框对应的线性地址。
*/
void *page_address(struct page *page)
{
unsigned long flags;
void *ret;
struct page_address_slot *pas;
/**
* 如果页框不在高端内存中(PG_highmem标志为0),则线性地址总是存在的。
* 并且通过计算页框下标,然后将其转换成物理地址,最后根据物理地址得到线性地址。
*/
if (!PageHighMem(page))
/**
* 本句等价于__va((unsigned long)(page - mem_map) << 12)
*/
return lowmem_page_address(page);
/**
* 否则页框在高端内存中(PG_highmem标志为1),则到page_address_htable散列表中查找。
*/
pas = page_slot(page);
ret = NULL;
spin_lock_irqsave(&pas->lock, flags);
if (!list_empty(&pas->lh)) {
struct page_address_map *pam;
list_for_each_entry(pam, &pas->lh, list) {
/**
* 在page_address_htable中找到,返回对应的物理地址。
*/
if (pam->page == page) {
ret = pam->virtual;
goto done;
}
}
}
/**
* 没有在page_address_htable中找到,返回默认值NULL。
*/
done:
spin_unlock_irqrestore(&pas->lock, flags);
return ret;
}
page_address()函数返回一个void *指针,指向给定物理页当前所在的逻辑地址。如果无应用到结构页,可以调用:
usigned long __get_free_pages(gfp_t gfp_mask,unsigned int order);
这个函数与alloc_pages()作用相同,不过_get_free_pages直接返回所请求的第一页的逻辑地址,分配2 ^顺序页。因为页是连续的,所以其他页会在其之后。
fastcall unsigned long __get_free_pages(unsigned int gfp_mask, unsigned int order)
{
struct page * page;
page = alloc_pages(gfp_mask, order);
if (!page)
return 0;
return (unsigned long) page_address(page);
}
而函数__get_free_page 只分配一页,返回指向其逻辑地址的指针。
4.获得填充为0的页
get_zeroed_page()
/**
* 用于获取填满0的页框。
*/
fastcall unsigned long get_zeroed_page(unsigned int gfp_mask)
{
struct page * page;
/*
* get_zeroed_page() returns a 32-bit address, which cannot represent
* a highmem page
*/
BUG_ON(gfp_mask & __GFP_HIGHMEM);
page = alloc_pages(gfp_mask | __GFP_ZERO, 0);
if (page)
return (unsigned long) page_address(page);
return 0;
}
这个函数与__get_free_pages()工作方式相同,只不过把分配好的页都填充成了0。
get_zeored_pa ge()函数只分配一页,返回指向其逻辑地址的指针。
在用户空间的页面返回之前,所有数据都必须填充为0,或做其他清理工作。
5.释放页
首先检查页面指向的页描述符。
如果该页框未被保留,就把描述符的计数字段减1;
如果计数变为0时,就假定从与页对应的页框开始的2 ^顺序个连续页框不再被使用。这种情况下,该函数释放页框。
fastcall void __free_pages(struct page *page, unsigned int order)
{
if (!PageReserved(page) && put_page_testzero(page)) {
if (order == 0)
free_hot_page(page);
else
__free_pages_ok(page, order);
}
}
free_pages()函数类似于__free_pages,但是它的接收参数为要释放的第一个页框的线性地址。
fastcall void free_pages(unsigned long addr, unsigned int order)
{
if (addr != 0) {
BUG_ON(!virt_addr_valid((void *)addr));
__free_pages(virt_to_page((void *)addr), order);
}
}
-------------------------------------------------------------------------------------------
补充:
高端内存的映射
永久映射
要映射一个给定的页面结构体到内核地址空间,使用函数的KMAP()。
void * kmap(struct page * page)l
该函数在高端内存或低端内存上都可用。可睡眠,只能用于进程上下文中。
如果页面结构对应的是低端内存中的一页,函数只会返回该页的虚拟地址;
如果页位于高端内存,则会建立一个永久映射,再返回地址。
2.临时映射
当必须创建一个映射而当前的上下文又不能睡眠是,内核提供了临时映射(也就是原子映射)。
柯林斯用在不能睡眠的地方,中断比如处理程序中,因为获取映射时不会阻塞。
通过函数kmap_atomic建立一个临时映射:
void * kmap_atomic(struct page * page,enum km_type type);
函数该不会阻塞,可用在中断上下文和其他不能调度的地方。禁止内核抢占,因为映射对于每个处理器都是唯一的。
-------------------------------------
参考资料:
《Linux内核设计与实现》