Linux内核设计与实现---内存管理

1 页

内核把物理页作为内存管理的基本单元,内存管理单元(MMU)以页为单位来管理系统中的页表,从虚拟内存的角度看,页就是最小单位。

体系结构不同,支持页的大小也不尽相同,还有些体系结构甚至支持几种不同的页大小。大多数32位体系结构支持4KB的页,而64位体系结构一般会支持8KB的页。

内核用struct page结构表示系统中的每个物理页,该结构位于linux/mm.h中。

struct page {
	page_flags_t flags;		/* Atomic flags, some possibly
					 * updated asynchronously */
	atomic_t _count;		/* Usage count, see below. */
	atomic_t _mapcount;		/* Count of ptes mapped in mms,
					 * to show when page is mapped
					 * & limit reverse map searches.
					 */
	unsigned long private;		/* Mapping-private opaque data:
					 * usually used for buffer_heads
					 * if PagePrivate set; used for
					 * swp_entry_t if PageSwapCache
					 */
	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 */
};

flags域用来存放页的状态,flags的每一位单独表示一种状态,page_flags_t是 unsigned long类型,所以它至少可以同时表示出32种不同的状态。这些表示定义在linux/page-flags.h中。

_count域存放页的引用计数,也就是这一页被引用了多少次。当数值为0时,说明当前内核并没有引用这一页,于是,在新的分配中就可以使用它。内核代码不应当直接检查该域,而是调用page_count()进行检查,该函数唯一的参数就是page结构。当页空闲时,返回0,返回一个正整数表示页在使用。一个页可以由页缓存使用,这是mapping指向和这个页关联的address_sapce对象,或者作为私有数据,由private指向,或者作为进程页表中的映射。

virtual域是页的虚拟地址。通常情况下,它就是页在虚拟内存中的地址,高端内存并不永久地映射到内核地址空间上,在这种情况下,这个域的值为NULL。

page结构与物理页相关,而并非与虚拟页相关,内核仅仅用这个数据结构来描述当前时刻在相关的物理页中存放的东西,系统中的每个物理页都要分配一个这样的结构

2 区

由于硬件的限制,内核并不能对所有的页一视同仁,内核把页划分为不同的区。Linux使用了三种区:

  • ZONE_DMA:这个区包含的页能用来执行DMA操作
  • ZONE_NOEMAL:这个区包含的都是能正常映射的页
  • ZONE_HIGHMEM:这个区包含高端内存,其中的页能不永久映射到内核地址空间。

这些区定义位于linux/mmzone.h。区的实际使用和分布是与体系结构相关的。在x86上的区分布如下表:
在这里插入图片描述
注意,区的划分没有任何意义,这只不过是内核为了管理页面而采取的一种逻辑上的分组。

每个区都用struct zone表示,定义在linux/mmzone.h中

struct zone {
	/* Fields commonly accessed by the page allocator */
	unsigned long		free_pages;
	unsigned long		pages_min, pages_low, pages_high;
	/*
	 * protection[] is a pre-calculated number of extra pages that must be
	 * available in a zone in order for __alloc_pages() to allocate memory
	 * from the zone. i.e., for a GFP_KERNEL alloc of "order" there must
	 * be "(1<<order) + protection[ZONE_NORMAL]" free pages in the zone
	 * for us to choose to allocate the page from that zone.
	 *
	 * It uses both min_free_kbytes and sysctl_lower_zone_protection.
	 * The protection values are recalculated if either of these values
	 * change.  The array elements are in zonelist order:
	 *	[0] == GFP_DMA, [1] == GFP_KERNEL, [2] == GFP_HIGHMEM.
	 */
	unsigned long		protection[MAX_NR_ZONES];

	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 */

	/*
	 * 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.
	 *
	 * temp_priority is used to remember the scanning priority at which
	 * this zone was successfully refilled to free_pages == pages_high.
	 *
	 * Access to both these fields is quite racy even on uniprocessor.  But
	 * it is expected to average out OK.
	 */
	int temp_priority;
	int prev_priority;


	ZONE_PADDING(_pad2_)
	/* Rarely used or read-mostly fields */

	/*
	 * wait_table		-- the array holding the hash table
	 * wait_table_size	-- 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_size;
	unsigned long		wait_table_bits;

	/*
	 * Discontig memory support fields.
	 */
	struct pglist_data	*zone_pgdat;
	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:
	 */
	char			*name;
} ____cacheline_maxaligned_in_smp;

系统中只有三个区,因此也只有三个这样的结构,frr_pages域是这个区中空闲页的个数,name是一个以NULL结束的字符串,表示这个区的名字。内核启动期间初始化这个值,其代码位于mm/page-alloc.c中,三个区的名字分别为DMA、Normal、HighMem。

3 获得页

static inline struct page *
alloc_pages(unsigned int gfp_mask, unsigned int order);

gfp_mask会在文章的gfp_mask节介绍。
该函数分配2的order次方个连续的物理页,并返回一个指针,该指针指向第一个页page结构体,如果出错,返回NULL。可以使用下面这个函数把给定的页转为它的逻辑地址:

void *page_address(struct page *page);

该函数返回一个指针,指向给定物理页当前所在的逻辑地址。如果你无须用到struct page,你可以调用:

unsigned long __get_free_pages(unsigned int gfp_make,unsigned int order);

这个函数与alloc_pages()作用相同,不过它直接返回所请求的第一个页的逻辑地址。

如果你只需要一页,可以用下面两个封装好的函数:

#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0);
#define __get_free_page(gfp_mask) \
		__get_free_pages((gfp_mask),0);

获得填充为0的页

如果你需要让返回的页的内容全为0,请用下面这个函数:

extern unsigned long FASTCALL(get_zeroed_page(unsigned int gfp_mask));

这个函数与_get_free_page()工作方式相同,只不过把分配好的页都填充成了0。

所有底层的页分配方法如下:
在这里插入图片描述

释放页

当你不在虚拟页时,可以用下面的一族函数来释放它们:
在这里插入图片描述
释放页时需谨慎,只能释放属于自己的页,传递了错误的struct page或地址,用错了order值,这些都可能导致系统崩溃,内核时完全信赖自己的,上面都能执行成功。

4 kmalloc()

kmalloc()函数与用户空间的malloc()一族函数非常类似,只不过它多了一个flags参数。kmalloc函数是一个简单的接口,用它可以获得以字节为单位一块内核内存。如果你需要整个页,那么,前面讨论的页分配接口可能是更改的接口。
kmalloc()在linux/slab.h中申明:

void *kmalloc(size_t size, int flags);
  • size:是指要分配的内存的字节数。
  • flags:是分配标志,它提供了多种kmalloc( )的行为。在下面的gfp_mask标志节会介绍。

这个函数返回一个指向内存块的指针,其内存块至少要有size大小,所分配的内存区在物理上是连续的,在出错时,它返回NULL,除非没有足够的内存可用,否则内核总能分配成功。在使用kmalloc()时,必须检查返回值是不是NULL。

为什么是至少分配size大小?因为内核分配内存是基于页的,因此在可用内存内,某些分配可能向下取整。

gfp_mask标志

不管在低级页分配函数中,还是在kmalloc()中,都用到了分配器标志。现在,我们深入讨论以一下这些标志。这些标志可分为三类:行为修饰符、区修饰符及类型。行为修饰符表示内核应当如何分配所需的内存,例如,中断处理程序就要求内核在分配内存的过程中不能睡眠。区修饰符表示从哪儿分配内存。类型标志组合了行为修饰符和区修饰符,将各种可能用到的组合归纳为不同类型。
所有这些标志都是在linux/gfp.h中申明的。

  1. 行为修饰符
    在这里插入图片描述

  2. 区描述符
    内核优先从ZONE_NORMAL开始,这样可以确保其他区在需要时有足够的空闲页可供使用。
    在这里插入图片描述
    你不能给_get_free_pages()或kmalloc()指定__GFP_HIGHMEM,因为这两个函数返回的都是逻辑地址,而不是page结构,这两个函数分配的内存当前有可能还没有映射到内核的虚拟地址空间,因此,可能根本没有逻辑地址,只有alloc_pages()才能分配高端地址。

  3. 类型标志
    在这里插入图片描述
    在这里插入图片描述

kfree()

kmalloc() 的另一端就是kfree(),kfree()声明在linux/slab.h中

void kfree(const void *ptr);

kfree释放由kmalloc分配出来的内存块。如果想要释放的内存不是由kmalloc()分配的,或者想要释放的内存早就被释放完了,调用这个函数会导致严重的后果。注意,调用kfree(NULL)是安全的。
在这里插入图片描述

5 vmalloc()

是malloc函数的工作方式类似于kmalloc(),只不过前者分配的内存虚拟地址是连续的,而物理地址无需连续。kmalloc函数确保页在物理地址上是连续的,vmalloc函数只确保页在虚拟地址空间内是连续的。它通过分配非连续的物理内存块,再修正页表。把内存映射到逻辑地址空间的连续区域中,就能做到这点。

vmalloc函数在linux/vmalloc.h中声明,在mm/vmalloc.c中定义。:

void *vmalloc(unsigned long size);

该函数返回一个指针,指向逻辑上连续的一块内存区,其大小至少为size。在发生错误时,函数返回NULL。函数可能睡眠,因此,不能从中断上下文中进行调用,也不能从其他不允许阻塞的情况下使用。

要释放通过vmalloc所获得的内存,使用下面的函数:

void vfree(void *addr);

这个函数会释放从addr开始的内存块,addr是由vmalloc分配的内存块地址。这个函数也可以睡眠,因此,不能从中断上下文中调用,它没有返回值。
在这里插入图片描述

6 slab层

分配和释放数据结构时所有内核中最普通的操作之一,为了便于数据的频繁分配和回收,编程者常常会用到一个空闲链表。该空闲链表包含有可供使用的、已经分配好的数据结构块。当需要一个新的数据结构实例时,就可以从空闲链表中抓取一个,而不需要分配内存,以后,当不再需要这个数据结构的实例时,就把它放回空闲链表,而不是释放它。从某种意义上说,空闲链表相当于数据结构实例高速缓存,以便快速存储频繁使用的对象实例。
Linux内核提供了slab层(slab分配器),slab分配器扮演了通用数据结构缓冲层的角色。

slab层的设计

每个高速缓存被划分为slab,每个高速缓存可以由多个slab组成,slab由一个或多个物理上连续的页组成,每个slab都包含一些对象成员,这里的对象是被缓存的数据结构,每个slab处于三种状态之一:慢、部分满或空。当内核的某一部分需要一个新的对象时,先从部分满的slab中进行分配,如果没有部分满的slab,就从空的slab中进行分配,如果没有空的slab,就要创建一个slab了。

作为一个例子,让我们考察一下inode结构,该结构时磁盘索引节点在内存的体现,这些数据会频繁地创建和释放,因此,用slab分配器来管理它们就很有必要。因为struct inode就用inode_cachep进行分配的,这种高速缓存由一个或多个slab组成,每个slab包含尽可能多的struct inode对象。当内核请求分配一个新的inode结构时,内核就从部分满的slab或空slab返回一个指向已分配但未使用的结构指针。当内核用完inode对象后,slab分配就就把该对象标记为空闲。高速缓存、slab和对象之间的关系:
在这里插入图片描述
每个高速缓存都是用kmem_cache_s结构来表示,这个结构包含三个链表slabs_full、slabs_partial和slab_empty,均放在kmem_list3结构内。这些链表包含高速缓存中所有的slab。slab描述符struct slab用来描述每个slab。slab分配器可以创建新的slab,这是通过__get_free_pages()低级内核页分配器实现的。

7 slab分配器的接口

一个新的高速缓存是通过以下函数创建的:

kmem_cache_t *kmem_cache_create(const char *name, size_t size, size_t align, unsigned long flags,
				       void (*ctor)(void *, kmem_cache_t *, unsigned long),
				       void (*dtor)(void *, kmem_cache_t *, unsigned long));

第一个参数是字符串,存放着高速缓存的名字。第二个参数是高速缓存中每个元素的大小,第三个参数就是高速缓存内第一个对象的偏移,这用来确保在页内进行特定的对齐,通常情况,0就可以满足要求,也就是标准对齐。flags是可选的设置项,用来控制高速缓存的行为。

最后两个参数ctor和dtor分别是高速缓存的构造和析构函数。只有在新的页追加到高速缓存时,构造函数才被调用。只有从高速缓存中删去页时,析构函数才被调用。有一个析构函数就要有一个构造函数,实际上,Linux内核的高速缓存不使用析构函数或构造函数,可以将这两个参数都赋值为NULL。

kmem_cache_create()在成功时会返回一个指向所创建高速缓存的指针,否则,返回NULL。这个函数不能在中断上下文中调用,因为它可能会睡眠。

要销毁一个高速缓存,则调用:

int kmem_cache_destroy(kmem_cache_t *cachep);

同样,也不能从中断上下文中调用这个函数,因为它也可能会睡眠。调用该函数之前必须确保以下两个条件:

  • 高速缓存中的所有slab都必须为空
  • 在调用kmem_cache_destroy()期间,不能再访问这个高速缓存

创建高速缓存之后,就可以通过下列函数从中获取对象:

void *kmem_cache_alloc(kmem_cache_t *cachep,int flags);

该函数从给定的高速缓存cachep中返回一个指向对象的指针。如果高速缓存的所有slab中都没有空闲的对象,那么slab层必须通过kmem_getpages()获取新的页,flags的值传递给__get_free_pages()。

最后释放一个对象,并把它返回给原先的slab,可以使用下面的函数:

void kmem_cache_free(kmem_cache_t *cachep,void *objp);

这样就能把高速缓存cachep中的对象objp标记为空闲了。

8 在栈上的静态分配

内核栈可以是1页,也可以是两页,这取决于编译时配置选项,栈大小因此在4KB-16KB的范围内。历史上,中断处理程序和被中断进程共享一个内核栈,当1页栈的选项被激活时,中断处理程序就获得了自己的栈。

9 高端内核的映射

根据定义,在高端内存中的页不能永久映射到内核地址空间上。在x86体系结构上,高于896的所有物理内存的范围大都是高端内存,它并不会永久地或自动映射到内核地址空间。

永久映射

要映射一个给定的page结构到内核地址空间,可以使用:

void *kmap(struct page *page);

这个函数在高端内存或低端内存都能用。如果page结构对应的是低端内存中的一页,函数只会单纯地返回该页的虚拟地址。如果页位于高端内存,则会建议一个永久映射,再返回地址。这个函数可以睡眠。因此kmap()只能用在进程上下文中。
当不再需要高端内存时,应该解除映射,可以通过kunmap函数完成:

void kunmap(struct page *page);

临时映射

当必须创建一个映射而当前上下文又不能睡眠时,内核提供了临时映射(也就是所谓的原子映射)。有一组保留的映射,它们可以存放新创建的临时映射,内核可以原子地把高端内存中的一个页映射到某个保留的映射中。因此,临时映射可以用在不能睡眠的地方,比如中断处理程序中,因为获取映射时绝不会阻塞。
可以通过下列函数创建一个临时映射:

void *kmap_atomic(struct page *page,enum km_type type);

可通过下列函数取消映射:

void kunmap_atomic(void *kvaddr.enum km_type type);

这个函数也不会阻塞。

10 每个CPU的分配

支持SMP的现代操作系统使用每个CPU上的数据,对于给定的处理器其数据时唯一的,每个CPU的数据存放在一个数组中,数组中的每一项对应着系统上一个存在的处理器,当前处理器号确定这个数组的当前元素,这就是2.4内核处理每个CPU数据的方式。
可以像下面这样声明数据:

unsigned long my_percou[NR_CPUS];

然后,按如下方式访问它:

int cpu;
cpu = get_cput;	/* 获取当前处理器号,并禁止内核抢占 */
my_percpu[cpu]++;	/* 访问当前处理器的数据 */

put_cpu();		/* 激活内核抢占 */

11 新的每个CPU的接口

2.6内核为了方便创建和操作每个CPU数据,从而引进了新的操作接口,称作percpu。该接口归纳了前面所述的操作行为,并使每个CPU数据的创建和操作得以简化。头文件linux/percpu.h声明了所有的接口操作例程,可以在文件mm/slab.c和asm/percput.h中找到它们的定义。

编译时的每个CPU数据

在编译时定义每个CPU变量很容易:

DEFINE_PER_CPU(type,name);

这个语句为系统中的每个处理器都创建一个类型为type,名字为name的变量实例。如果你需要在别处声明变量,以防编译时警告,那么下面的宏将是你的好帮手:

DECLARE_PER_CPU(type,name);

你可以用get_cpu_var()和put_cpu_var()例程操作变量。调用get_cpu_var()返回当前处理器上的指定变量,同时它将禁止抢占,另一方面put_cpu_var()将相应地重新激活抢占。
在这里插入图片描述
这些编译时每个CPU数据的例子并不能在模块内使用,因为连接程序实际上将它们创建在一个唯一的可执行段中(.data.percpu)。

运行时每个CPU数据

内核实现每个CPU数据的动态分配方法类似于kmalloc()。这些方法原型在文件linux/percpu.h中
在这里插入图片描述
宏alloc_percpu()给系统中的每个处理器分配一个指定类型对象的实例。它其实是宏__alloc_percpu()的一个封装,这个原始宏接受的参数有两个,一个是要分配的实际字节数,一个是分配时按多少字节对齐。而封装后的alloc_percpu安装给定类型的自然边界对齐。比如:
在这里插入图片描述

相应调用free_percpu()将释放所有处理器上指定的每个CPU数据。

无论是alloc_percpu()还是__alloc_percpu()都会返回一个指针,它用来间接引用动态创建的每个CPU数据,内核提供了两个宏来利用指针获取每个CPU数据:

get_cpu_ptr(ptr);
put_cpu_ptr(ptr);

get_cpu_ptr()宏返回一个指向当前处理器数据的实例,它同时会禁止内核抢占,而在put_cpu_ptr宏中重新激活内核抢占。
我们来看一个例子
在这里插入图片描述

12 使用每个CPU数据的原因

  1. 减少了数据锁定:因为按照么个处理器访问每个CPU数据的逻辑,你可以不在需要任何锁。
  2. 使用每个CPU数据可以大大减少缓存失效。失效发生在处理器试图使它们的缓存保持同步时,如果一个处理器操作某个数据,而该数据又存放在其他处理器缓存中,那么存放该数据的那个处理器必须清理或刷新它自己的缓存。使用每个CPU数据将使缓存影响降至最低,因为理想情况下只会访问它自己的数据。percpu接口缓存对齐所有数据,以便确保在访问一个处理器的数据时,不会将另一个处理器的数据带入同一个缓存线上。

如果真决定在你的内核中使用每个CPU数据,请考虑使用新街口,但是新接口不向后兼容老内核。

13 分配函数的选择

  1. 如果你需要连续的物理页,就可以使用某个低级页分配器或kmalloc。
  2. 如果你想从高端内存进行分配,就可以alloc_pages()。该函数返回一个指向struct page结构的指针,而不是一个指向某个逻辑地址的指针。
  3. 如果你不需要物理上的连续的页,而仅仅需要虚拟地址上连续的页,那么就使用vmalloc()。
  4. 如果你需要创建和销毁很多较大的数据结构,那么应该考虑建立slab高速缓存。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值