<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Soul Of Free Loop &#187; page</title>
	<atom:link href="https://zohead.com/archives/tag/page/feed" rel="self" type="application/rss+xml" />
	<link>https://zohead.com</link>
	<description>Uranus Zhou&#039;s Blog</description>
	<lastBuildDate>Sat, 19 Jul 2025 15:42:46 +0000</lastBuildDate>
	<language>zh-CN</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.8</generator>
	<item>
		<title>Linux kernel学习-内存管理</title>
		<link>https://zohead.com/archives/linux-kernel-learning-memory-management/</link>
		<comments>https://zohead.com/archives/linux-kernel-learning-memory-management/#comments</comments>
		<pubDate>Sat, 30 Jun 2012 19:03:50 +0000</pubDate>
		<dc:creator><![CDATA[Uranus Zhou]]></dc:creator>
				<category><![CDATA[kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[代码分析]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[kmalloc]]></category>
		<category><![CDATA[kmap]]></category>
		<category><![CDATA[page]]></category>
		<category><![CDATA[内存]]></category>
		<category><![CDATA[分配]]></category>
		<category><![CDATA[编程]]></category>
		<category><![CDATA[释放]]></category>
		<category><![CDATA[高端内存]]></category>

		<guid isPermaLink="false">http://zohead.com/?p=242</guid>
		<description><![CDATA[本文同步自（如浏览不正常请点击跳转）：https://zohead.com/archives/linux-kernel-learning-memory-management/ 接着之前的 Linux kernel 学习步伐，来到极其重要的内存管理部分，继续本文内容，需要先了解内存寻址的基础知识，见之前的 [内存寻址] 博文。 1、内存页及内存区域： 正如之前所说，Linux kernel 使用物理页作为内存管理的基本单位，其中重要的线程地址和物理地址的转换操作由页单元 MMU 来完成，系统的页表也由 MMU 来维护。kernel 使用 struct page 来表示一个物理页，它的定义在 in [&#8230;]]]></description>
				<content:encoded><![CDATA[<p>本文同步自（如浏览不正常请点击跳转）：<a href="https://zohead.com/archives/linux-kernel-learning-memory-management/" target="_blank">https://zohead.com/archives/linux-kernel-learning-memory-management/</a></p>
<p>接着之前的 Linux kernel 学习步伐，来到极其重要的内存管理部分，继续本文内容，需要先了解内存寻址的基础知识，见之前的 [<a href="https://zohead.com/archives/linux-kernel-learning-memory-addressing/" target="_blank">内存寻址</a>] 博文。</p>
<p><strong><span style="color: #ff0000;">1、内存页及内存区域：</span></strong></p>
<p>正如之前所说，Linux kernel 使用物理页作为内存管理的基本单位，其中重要的线程地址和物理地址的转换操作由页单元 MMU 来完成，系统的页表也由 MMU 来维护。kernel 使用 struct page 来表示一个物理页，它的定义在 include/linux/mm_types.h 头文件中：</p>
<pre class="brush: cpp; title: include/linux/mm_types.h; notranslate">
struct page {
	unsigned long flags;		/* Atomic flags, some possibly
					 * updated asynchronously */
	atomic_t _count;		/* Usage count, see below. */
	union {
		atomic_t _mapcount;	/* Count of ptes mapped in mms,
					 * to show when page is mapped
					 * &amp; limit reverse map searches.
					 */
		struct {		/* SLUB */
			u16 inuse;
			u16 objects;
		};
	};
	union {
	    struct {
		unsigned long private;		/* Mapping-private opaque data:
					 	 * usually used for buffer_heads
						 * if PagePrivate set; used for
						 * swp_entry_t if PageSwapCache;
						 * indicates order in the buddy
						 * system if PG_buddy is set.
						 */
		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.
						 */
	    };
#if USE_SPLIT_PTLOCKS
	    spinlock_t ptl;
#endif
	    struct kmem_cache *slab;	/* SLUB: Pointer to slab */
	    struct page *first_page;	/* Compound tail pages */
	};
	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-&gt;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 ... <img src="https://zohead.com/wp-includes/images/smilies/icon_wink.gif" alt=";)" class="wp-smiley" /> 
	 *
	 * 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 */
#ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS
	unsigned long debug_flags;	/* Use atomic bitops on this */
#endif

#ifdef CONFIG_KMEMCHECK
	/*
	 * kmemcheck wants to track the status of each byte in a page; this
	 * is a pointer to such a status block. NULL if not tracked.
	 */
	void *shadow;
#endif
};
</pre>
<p>其中的 flags 用于表示页的状态（是否为脏或者被锁定等），_count 即为页的引用计数，kernel 一般使用 page_count 宏调用 atomic_read 函数原子的读取此值，page_count 返回 0 表示此页可用。如果一个页被作为 page cache 使用，则 page 的 mapping 字段指向映射的 inode 的 address_space 对象，如果页被作为私有数据（作为 buffer_heads 缓冲、buddy 系统等），则 private 常包含对应的信息。注意其中的 virtual 字段为页的虚拟地址，结合之前的知识，对于高端内存来说，其并没有被固定映射到 kernel 地址空间中，因此如果 virtual 字段为 NULL，则表示此页必须被动态映射。</p>
<p>kernel 使用 page 结构记录系统中的所有页，因此 struct page 的大小应该要尽量小以减少内存占用，另外 kernel 必须知道页是否空闲，如果不空闲则拥有者是谁。</p>
<p>由于实际硬件限制，Linux kernel 不可能使用全部的物理内存，kernel 为此将内存划分为不同的区域，一个区域中的内存属性应该也相同。kernel 中常见的内存区域有 ZONE_DMA（可用于 DMA 的页）、ZONE_DMA32（与 ZONE_DMA 类似，但只对 32 位设备可用）、ZONE_NORMAL、ZONE_HIGHMEM（并没有被固定映射的高端内存区域），这些内存区域一般都是硬件相关的，例如在 x86 架构下，ZONE_DMA 的范围为 0MB - 16MB，ZONE_HIGHMEM 为高于 896MB 的物理内存，而在 x86_64 架构下 ZONE_HIGHMEM 则为空。需要注意的是内存的分配不会跨域这些不同的内存区域。内存区域在 kernel 中由 struct zone 结构来表示，其中的 name 字段即为内存区域名称。</p>
<p><strong><span style="color: #ff0000;">2、获取页：</span></strong></p>
<p>分配和释放内存是 Linux kernel 中极其重要又用的极多的接口。先看看 kernel 提供的直接获取以内存页面为单位的 alloc_pages 函数：</p>
<p><em><span style="color: #008000;">struct page * alloc_pages(gfp_t gfp_mask, unsigned int order)</span></em></p>
<p>此函数是最基本的用于分配大小为 2^order 并且连续的物理页的函数，其返回分配到的第一个页面的 page 指针。</p>
<p>来看看比较重要的 gfp_t 类型的 gfp_mask 值：</p>
<p>gfp_t 实际上就是 unsigned int 类型，gfp_mask 常用于指定行为方式、区域方式、类型等信息。常见的行为方式标志有：__GFP_WAIT（标志分配器可以睡眠，明显不适用于中断上下文中）、__GFP_IO（分配器可以启动磁盘 I/O）等。区域方式指定内存从哪里分配，对应的就有：__GFP_DMA、__GFP_DMA32、__GFP_HIGHMEM（从高端内存或普通内存中分配）。类型标志则用于简化分配时的指定操作，常见的有：GFP_ATOMIC（高优先级并不可睡眠，常用于中断、中断下半部、持有自旋锁等环境中）、GFP_NOIO（表示分配可中断但不可以发起 I/O 操作）、GFP_NOFS（分配时不可发起文件 I/O 操作）、GFP_KERNEL（最常见的分配标志，常用于可以睡眠的进程上下文中）、GFP_USER（用于分配内存给用户进程）、GFP_DMA 等。</p>
<p>需要注意的是对 __get_free_pages 和 kmalloc 函数（下面会分别说明）不能指定 __GFP_HIGHMEM 标志，因为它们都是直接返回的虚拟地址，而非 page 结构指针，如果指定了 __GFP_HIGHMEM，则他们可能分配到的内存并没有被映射到 kernel 地址空间，因此这样得不到虚拟地址。只有 alloc_page 函数可以分配高端内存，这个限制在下面的 __get_free_pages 函数的实现中可以看到。</p>
<p>使用 page_address 函数可以将 page 指针转换为虚拟地址（非物理地址）。实际使用中经常会用到 __get_free_pages 函数直接在分配页时直接得到虚拟地址，其参数为 alloc_pages 完全一样，看看它的实现就一目了然了：</p>
<pre class="brush: cpp; title: mm/page_alloc.c; notranslate">
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
	struct page *page;

	/*
	 * __get_free_pages() returns a 32-bit address, which cannot represent
	 * a highmem page
	 */
	VM_BUG_ON((gfp_mask &amp; __GFP_HIGHMEM) != 0);

	page = alloc_pages(gfp_mask, order);
	if (!page)
		return 0;
	return (unsigned long) page_address(page);
}
</pre>
<p>另外 kernel 还 “好心” 的提供了两个只分配一个页的函数：alloc_page 和 __get_free_page，可以想象只是把 order 参数设为 0 而已。你可以使用 get_zeroed_page 函数分配一个页并自动清零（gfp_mask 指定 __GFP_ZERO）。</p>
<p>对应的释放页可以用 __free_pages（page 指针为参数）、free_pages（虚拟地址为参数）、free_page（只释放一个页）这些函数。</p>
<p>下面是常用的分配非整数倍页大小的内存的函数。首先是最常用的 kmalloc 函数：</p>
<p><em><span style="color: #008000;">void *kmalloc(size_t size, gfp_t flags)</span></em></p>
<p>kmalloc 用于分配最少指定的 size 字节大小的内存（实际分配的可能比 size 多），这与用户空间的 malloc 函数很相似，但需要注意的是 kmalloc 分配的内存物理地址是连续的，这非常重要。</p>
<p>相应的释放内存函数是 kfree：</p>
<p><em><span style="color: #008000;">void kfree(const void *objp)</span></em></p>
<p>kfree 用于释放 kmalloc 分配的内存，注意如果使用 kfree 在不是的 kmalloc 分配的内存地址或者已经 kfree 过的地址上，都可能导致 kernel 出错。</p>
<p>紧接着就是大名鼎鼎的 vmalloc 函数了。它与 kmalloc 类似，但它分配的内存只是虚拟连续的而物理地址却不一定连续，这也类似于用户空间的 malloc 函数的效果。vmalloc 由于需要做页表转换之类的操作，性能比 kmalloc 差，而且 vmalloc 得到的页还必须由单独的页来做映射，对 TLB 缓存的效率也会有影响（有关 TLB 缓存参考之前的文章 [<a href="https://zohead.com/archives/linux-kernel-learning-memory-addressing/" target="_blank">内存寻址</a>]），由于这些原因，vmalloc 在 kernel 中用到的机会并不是很多，其常用于分配大量的内存，常见的一个例子就是内核模块的代码就是通过 vmalloc 加载到 kernel 中的。vmalloc 的原型为：</p>
<p><em><span style="color: #008000;">void * vmalloc(unsigned long size)</span></em></p>
<p>与之对应的，使用 vfree 释放分配的内存。另外 vmalloc 和 vfree 都是可以睡眠的，因此它们对中断上下文是不适用的。</p>
<p><strong><span style="color: #ff0000;">3、Slab分配器：</span></strong></p>
<p>Slab 也是 Linux kernel 中非常重要的组成部分，它用于简化内存的分配和释放，它相当于一个可用内存列表，里面包含一堆已经分配好的数据结构，当 kernel 需要分配一个数据结构时，可以直接从这个可用内存列表中取出而节省分配的时间，不需要的时候又可以还给这个列表而不需要释放，因此这个列表用于缓存经常访问的某种类型的数据。为了统一管理和释放，Linux kernel 引入 Slab 分配器作为通用的数据结构缓存层给经常访问的数据结构使用。需要说明的是 kmalloc 就是在 Slab 分配器基础上实现的。</p>
<p>这里简单对 Slab 分配器做个介绍，有关其细节请参考这篇 PDF 文档：</p>
<p><a href="http://students.mimuw.edu.pl/SO-MSUI/Wyklady/06_pamiec/bonwick.pdf" target="_blank">The Slab Allocator: An Object-Caching Kernel Memory Allocator</a></p>
<p>Slab 层将不同的对象划分到名为 cache 的不同组中，每个组存储不同类型的数据，也就是每种数据类型都有一个 cache。每个 cache 然后被划分为多个 slab，slab 由一个或多个连续的物理页组成（通常只有一个页），每个 slab 又包含一些数量的对象，也就是实际缓存的数据。每个 slab 的状态可以是这三个中的一个：满、部分满、空。当 kernel 请求一个新对象时，优先从状态为 部分满 的 slab 中取，如果没有则从状态为 空 的 slab 中分配，如果没有状态为 空 的 slab 了就创建一个，可以看到这种策略可以相对的减少内存碎片。</p>
<p>kernel 中常用到的 struct inode 结构就是一个典型的例子，它在 VFS 等地方被用到的非常多，因此 kernel 中增加一个名为 inode_cachep 的 cache 用于缓存 inode 结构。</p>
<p>每个 cache 由 kmem_cache 结构来表示，它的 struct kmem_list3 *nodelists[MAX_NUMNODES] 类型字段即为该 cache 包含的所有 slab。每个 slab 由 struct slab 结构来表示，看看 kmem_list3 和 slab 结构的定义：</p>
<pre class="brush: cpp; title: mm/slab.c; notranslate">
struct slab {
	struct list_head list;
	unsigned long colouroff;
	void *s_mem;		/* including colour offset */
	unsigned int inuse;	/* num of objs active in slab */
	kmem_bufctl_t free;
	unsigned short nodeid;
};

struct kmem_list3 {
	struct list_head slabs_partial;	/* partial list first, better asm code */
	struct list_head slabs_full;
	struct list_head slabs_free;
	unsigned long free_objects;
	unsigned int free_limit;
	unsigned int colour_next;	/* Per-node cache coloring */
	spinlock_t list_lock;
	struct array_cache *shared;	/* shared per node */
	struct array_cache **alien;	/* on other nodes */
	unsigned long next_reap;	/* updated without locking */
	int free_touched;		/* updated without locking */
};
</pre>
<p>多个 slab 可以分别链接到 kmem_list3 的 满（slabs_full）、部分满（slabs_partial）、空（slabs_free）3 个链表中。</p>
<p>Slab 分配器调用 kmem_getpages 函数分配新的 slab（关于 cache 的创建下面会提到），kmem_getpages 会调用 __get_free_pages 函数分配所需的内存用于保持 cache，因此 kmem_getpages 一般在当 部分满（partial） 和 空（free）slab 的情况下调用，来看看它的实现：</p>
<pre class="brush: cpp; title: mm/slab.c; notranslate">
static void *kmem_getpages(struct kmem_cache *cachep, gfp_t flags, int nodeid)
{
	struct page *page;
	int nr_pages;
	int i;

#ifndef CONFIG_MMU
	/*
	 * Nommu uses slab's for process anonymous memory allocations, and thus
	 * requires __GFP_COMP to properly refcount higher order allocations
	 */
	flags |= __GFP_COMP;
#endif

	flags |= cachep-&gt;gfpflags;
	if (cachep-&gt;flags &amp; SLAB_RECLAIM_ACCOUNT)
		flags |= __GFP_RECLAIMABLE;

	page = alloc_pages_exact_node(nodeid, flags | __GFP_NOTRACK, cachep-&gt;gfporder);
	if (!page)
		return NULL;

	nr_pages = (1 &lt;&lt; cachep-&gt;gfporder);
	if (cachep-&gt;flags &amp; SLAB_RECLAIM_ACCOUNT)
		add_zone_page_state(page_zone(page),
			NR_SLAB_RECLAIMABLE, nr_pages);
	else
		add_zone_page_state(page_zone(page),
			NR_SLAB_UNRECLAIMABLE, nr_pages);
	for (i = 0; i &lt; nr_pages; i++)
		__SetPageSlab(page + i);

	if (kmemcheck_enabled &amp;&amp; !(cachep-&gt;flags &amp; SLAB_NOTRACK)) {
		kmemcheck_alloc_shadow(page, cachep-&gt;gfporder, flags, nodeid);

		if (cachep-&gt;ctor)
			kmemcheck_mark_uninitialized_pages(page, nr_pages);
		else
			kmemcheck_mark_unallocated_pages(page, nr_pages);
	}

	return page_address(page);
}
</pre>
<p>第一个参数 cachep 为需要分配页的 cache，cachep-&gt;gfporder 指定要分配的大小，上面的代码中对于 NUMA 架构做了必要的处理。</p>
<p>kmem_getpages 分配的内存通过 kmem_freepages 释放，它调用 free_pages 释放页，kmem_freepages 一般在系统检测到内存不足时调用或者在销毁 cache 时显示调用。</p>
<p>下面重点来看看 Slab 分配器如何使用。</p>
<p>使用 kmem_cache_create 函数创建新的 cache，其定义为：</p>
<p><em><span style="color: #008000;">struct kmem_cache * kmem_cache_create (const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void *))</span></em></p>
<p>第一个 name 参数指定 cache 的名称，size 为 cache 中对象的大小，align 为对象的对齐（一般为 0），flags 控制 cache 的行为，最后一个参数 ctor 为对象的构造函数，cache 分配新页时会调用此构造函数，现在一般将 ctor 值设为 NULL。</p>
<p>cache 的标志可以是下面常用几种标志的 OR 值：</p>
<p><strong>SLAB_HWCACHE_ALIGN</strong>：对 cache 中的每个对象做对齐处理，对齐之后可以提高 cache line 的访问性能，但由于要浪费内存空间，因此一般只在对性能有很高要求的场合使用；<br />
<strong>SLAB_POISON</strong>：以固定的值填充 slab（默认 0xa5a5a5a5）；<br />
<strong>SLAB_PANIC</strong>：如果分配失败，kernel 直接 panic；<br />
<strong>SLAB_CACHE_DMA</strong> ：指定 Slab 层在 ZONE_DMA 上分配每个 slab。</p>
<p>kmem_cache_create 如果成功返回 struct kmem_cache 结构指针，注意由于 kmem_cache_create 函数可能会睡眠，因此不能在中断上下文中使用。</p>
<p>使用 kmem_cache_destroy 函数销毁 kmem_cache_create 返回的 cache，此函数一般在模块退出时调用，你也可以在很多模块的初始化中找到 kmem_cache_create。同样由于会睡眠，kmem_cache_destroy 也不能在中断上下文中使用。</p>
<p>cache 被创建之后，就可以调用 kmem_cache_alloc 函数从 cache 中取得对象，其定义为：</p>
<p><em><span style="color: #008000;">void * kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)</span></em></p>
<p>此函数直接返回对象的指针，如果 cache 中所有 slab 都没有空闲的对象了，Slab 层就需要调用 kmem_getpages 获取新的页。</p>
<p>如果一个对象不再需要使用了，可以调用 kmem_cache_free 将其回收到 slab 中：</p>
<p><em><span style="color: #008000;">void kmem_cache_free(struct kmem_cache *cachep, void *objp)</span></em></p>
<p>需要注意的就是 kmem_cache_free 和 kmem_cache_destroy 不能混淆。</p>
<p><strong><span style="color: #ff0000;">4、高端内存映射：</span></strong></p>
<p>由于高端内存不是被固定映射到 kernel 地址空间中，因此 alloc_pages 函数使用时如果指定了 __GFP_HIGHMEM 标志，则它返回的 page 很可能没有有效的虚拟地址。</p>
<p>使用 kmap 函数可以将一个 page 固定的映射到 kernel 地址空间中：</p>
<p><em><span style="color: #008000;">void *kmap(struct page *page)</span></em></p>
<p>注意此函数对高端内存和低端内存都是适用的，如果 page 在低端内存，则直接返回页的虚拟地址，否则需要创建内存映射，由于 kmap 可能会睡眠，因此不能在中断上下文中使用。</p>
<p>被映射的高端内存不需要时应使用 kunmap 函数删除映射。</p>
<p>另外对于不能睡眠的进程环境，Linux kernel 又提供了临时的高端内存映射方法。kernel 可以原子地映射一个高端内存页到 kernel 中的保留映射集中的一个，此保留映射集也是专门用于中断上下文等不能睡眠的地方映射高端内存页的需要。临时高端内存映射函数为 kmap_atomic，看看它在 x86 下的实现：</p>
<pre class="brush: cpp; title: arch/x86/mm/highmem_32.c; notranslate">
void *kmap_atomic_prot(struct page *page, enum km_type type, pgprot_t prot)
{
	enum fixed_addresses idx;
	unsigned long vaddr;

	/* even !CONFIG_PREEMPT needs this, for in_atomic in do_page_fault */
	pagefault_disable();

	if (!PageHighMem(page))
		return page_address(page);

	debug_kmap_atomic(type);

	idx = type + KM_TYPE_NR*smp_processor_id();
	vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
	BUG_ON(!pte_none(*(kmap_pte-idx)));
	set_pte(kmap_pte-idx, mk_pte(page, prot));

	return (void *)vaddr;
}

void *kmap_atomic(struct page *page, enum km_type type)
{
	return kmap_atomic_prot(page, type, kmap_prot);
}
</pre>
<p>kmap_atomic 实际调用 kmap_atomic_prot 实现临时映射，kmap_atomic_prot 中同样会先做判断，如果要映射的页不在高端内存则直接返回虚拟地址，然后根据 type 和当前处理器 ID 计算得到 fixmap 的索引，并调用 __fix_to_virt 将 fixmap 索引转换为虚拟地址，有关 fixmap 机制见之前的 [<a href="https://zohead.com/archives/linux-kernel-learning-memory-addressing/" target="_blank">内存寻址</a>] 博文。</p>
<p>kmap_atomic 函数的 type 参数用于临时映射的用途，此函数会禁用内核抢占，因为临时映射是和每个处理器相关的，它是直接调用 pagefault_disable 函数禁止 page fault handler，其中会自动禁用内核抢占，看看 pagefault_disable 的实现：</p>
<pre class="brush: cpp; title: include/linux/uaccess.h; notranslate">
static inline void pagefault_disable(void)
{
	inc_preempt_count();
	/*
	 * make sure to have issued the store before a pagefault
	 * can hit.
	 */
	barrier();
}
</pre>
<p>临时高端内存映射可以使用 kunmap_atomic 函数删除，它会启用内核抢占，同时它也不会睡眠，需要注意的是此次的临时高端内存映射在下一次临时映射高端内存时就会无效。</p>
<p>本文中如果有任何问题，欢迎提出指正哦，玩的开心~~~ ^_^</p>
]]></content:encoded>
			<wfw:commentRss>https://zohead.com/archives/linux-kernel-learning-memory-management/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux kernel学习-内存寻址</title>
		<link>https://zohead.com/archives/linux-kernel-learning-memory-addressing/</link>
		<comments>https://zohead.com/archives/linux-kernel-learning-memory-addressing/#comments</comments>
		<pubDate>Fri, 25 May 2012 20:03:02 +0000</pubDate>
		<dc:creator><![CDATA[Uranus Zhou]]></dc:creator>
				<category><![CDATA[kernel]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[代码分析]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[CPU]]></category>
		<category><![CDATA[GDT]]></category>
		<category><![CDATA[LDT]]></category>
		<category><![CDATA[page]]></category>
		<category><![CDATA[TLB]]></category>
		<category><![CDATA[ULK]]></category>
		<category><![CDATA[内存]]></category>
		<category><![CDATA[学习]]></category>
		<category><![CDATA[寻址]]></category>
		<category><![CDATA[映射]]></category>
		<category><![CDATA[段式]]></category>
		<category><![CDATA[物理地址]]></category>
		<category><![CDATA[缓存]]></category>
		<category><![CDATA[虚拟地址]]></category>
		<category><![CDATA[页表]]></category>

		<guid isPermaLink="false">http://zohead.com/?p=180</guid>
		<description><![CDATA[近日在看 Understanding the Linux kernel（慢慢啃E文原版，以下简称 ULK），这本书虽然已经是第三版了，但它基于的 Linux kernel 版本却不是很新，现在 Linux kernel 都已经出到 3.4 版本了，这本书还是基于 2.6.11 的 kernel，不得不说 Linux kernel 的更迭速度太快了。 下面准备以我正在用的 2.6.34 版本的 kernel 为基础进行学习，这本书中不对应的地方我会尽量找到新 kernel 中的实现，并尽量自己做个了解，日后的相同日志如无意外也基于 2.6.34 版本 Linux kernel。 首先已完成第一章 [&#8230;]]]></description>
				<content:encoded><![CDATA[<p>近日在看 Understanding the Linux kernel（慢慢啃E文原版，以下简称 ULK），这本书虽然已经是第三版了，但它基于的 Linux kernel 版本却不是很新，现在 Linux kernel 都已经出到 3.4 版本了，这本书还是基于 2.6.11 的 kernel，不得不说 Linux kernel 的更迭速度太快了。</p>
<p>下面准备以我正在用的 2.6.34 版本的 kernel 为基础进行学习，这本书中不对应的地方我会尽量找到新 kernel 中的实现，并尽量自己做个了解，日后的相同日志如无意外也基于 2.6.34 版本 Linux kernel。</p>
<p>首先已完成第一章：Introduction（这一章没有 Linux kernel 代码），来到第二章 Memory Addressing，开始是介绍逻辑地址、线性地址、物理地址的对应关系，虽然之前用汇编写过 Linux 的 bootloader，用到过实模式和保护模式，但对 GDT、LDT 的概念并没有深入了解过。这一章开篇就介绍了 Intel 80X86 硬件上内存分段的实现，包括段选择子，段寄存器，段描述符。</p>
<h2 id="segmentation-linux">段式内存管理</h2>
<p>每个内存段由 8 个字节的段描述符来表示段的特征。段描述符被存储在 GDT 或者 LDT 中。内存中 GDT 的地址和大小包含在 gdtr 控制寄存器中，LDT 的地址和大小包含在 ldtr 控制寄存器中。段寄存器的高 13 位为段描述符在 GDT 或者 LDT 中的索引，GDT 或者 LDT 结构中包含基地址、段长度等信息。通过检查指令地址和段长度并确定没有越界以及权限是否正确之后，由于 线性地址 = 段基指 + 偏移地址，GDT 或者 LDT 中的基地址加上指令中的偏移量就可以得到需要的线性地址。</p>
<p>备注：由于每个进程都可以有 LDT，而 GDT 只有一个，为满足需求 Intel 的做法是将 LDT 嵌套在 GDT 表中。</p>
<h3 id="segmentation-linux-kernel">Linux kernel 中的内存分段</h3>
<p>Linux中所有进程使用相同的段寄存器值，因此它们的线性地址集也是相同的，不管在用户模式还是内核模式，都可以使用相同的逻辑地址，32位 kernel下为 4G 的地址空间。</p>
<p>ULK 中介绍的 user code、user data、kernel code、kernel data 这四个段对应的段选择子的宏为：__USER_CS、__USER_DS、__KERNEL_CS、__KERNEL_DS，2.6.11 中这4个宏定义在 include/asm-i386/segment.h 头文件中，2.6.34 中已经挪到 arch/x86/include/asm/segment.h 里，因为 2.6.34 中 i386 和 x86_64 的代码已经尽可能的合并到 x86 目录中，而不像老版本的代码那样弄成两个目录。定义如下：</p>
<pre class="brush: cpp; title: arch/x86/include/asm/segment.h; notranslate">
#define __KERNEL_CS	(GDT_ENTRY_KERNEL_CS*8)
#define __KERNEL_DS	(GDT_ENTRY_KERNEL_DS*8)
#define __USER_DS	(GDT_ENTRY_DEFAULT_USER_DS*8+3)
#define __USER_CS	(GDT_ENTRY_DEFAULT_USER_CS*8+3)
</pre>
<p>下面是 Linux kernel GDT 的实现：</p>
<p>由于 kernel 中每个内核需要有一个 GDT，因此就有一个 GDT table，ULK 中说的是存在 cpu_gdt_table 中，GDT 的地址和大小存在 cpu_gdt_descr 中，2.6.11 kernel 里都是放在 arch/i386/kernel/head.S，使用的地方：</p>
<pre class="brush: cpp; title: include/asm-i386/desc.h; notranslate">
extern struct desc_struct cpu_gdt_table[GDT_ENTRIES];
DECLARE_PER_CPU(struct desc_struct, cpu_gdt_table[GDT_ENTRIES]);

struct Xgt_desc_struct {
	unsigned short size;
	unsigned long address __attribute__((packed));
	unsigned short pad;
} __attribute__ ((packed));

extern struct Xgt_desc_struct idt_descr, cpu_gdt_descr[NR_CPUS];
</pre>
<p>到了 2.6.34 中已经改为：</p>
<pre class="brush: cpp; title: arch/x86/include/asm/desc.h; notranslate">
struct gdt_page {
	struct desc_struct gdt[GDT_ENTRIES];
} __attribute__((aligned(PAGE_SIZE)));
DECLARE_PER_CPU_PAGE_ALIGNED(struct gdt_page, gdt_page);

static inline struct desc_struct *get_cpu_gdt_table(unsigned int cpu)
{
	return per_cpu(gdt_page, cpu).gdt;
}
</pre>
<p>可以看到 2.6.34 中去掉了原来的 cpu_gdt_table 变量（详见 kernel commit <a href="http://git.kernel.org/?p=linux/kernel/git/stable/linux-stable.git;a=commit;h=bf50467204b435421d8de33ad080fa46c6f3d50b" target="_blank">bf50467204b435421d8de33ad080fa46c6f3d50b</a>），新增了一个 gdt_page 结构存放 GDT table，而且提供 get_cpu_gdt_table 函数取得某个 CPU 的 GDT。cpu_gdt_descr 也已去掉，新增了 desc_ptr 结构存放每个 CPU 的 GDT 信息，cpu_gdt_descr 也改为 early_gdt_descr。</p>
<pre class="brush: cpp; title: arch/x86/include/asm/desc_defs.h; notranslate">
struct desc_ptr {
	unsigned short size;
	unsigned long address;
} __attribute__((packed)) ;
</pre>
<p>看下简单看下新的切换 GDT 的实现：</p>
<pre class="brush: cpp; title: arch/x86/kernel/cpu/common.c; notranslate">
/*
 * Current gdt points %fs at the &quot;master&quot; per-cpu area: after this,
 * it's on the real one.
 */
void switch_to_new_gdt(int cpu)
{
	struct desc_ptr gdt_descr;

	gdt_descr.address = (long)get_cpu_gdt_table(cpu);
	gdt_descr.size = GDT_SIZE - 1;
	load_gdt(&amp;gdt_descr);
	/* Reload the per-cpu base */

	load_percpu_segment(cpu);
}
</pre>
<p>load_gdt 最终调用 lgdt 汇编指令。</p>
<h2 id="paging-linux">页式内存管理</h2>
<p>Intel 从 80386 开始支持页式内存管理，页单元将线性地址翻译为物理地址。当 CR0 控制寄存器中的 PG 位置为 1 时，启动分页管理功能，为 0 时，禁止分页管理功能，并且把线性地址作物理地址使用。</p>
<p>32 位线性地址的高 10 位为页表目录的下标（指向页表），中间 10 位为页表的下标（指向页面），低 12 位为该地址在页面（通常大小为 4 KB）中的偏移量，这样的二层寻址设计主要为了减少页表本身所占用的内存，由于页表目录和页表都为 10 位，因此都最多包含 1024 个项。正在使用的页表目录的物理地址存在 cr3 控制寄存器中。</p>
<p>在 32 位大小的页表目录（页表）的结构中，其高 20 位为页表（页面）基地址的高 20 位，其它的 flag 中包含一个 Present 标志，如果该值为 1，表示指向的页面或者页表在内存中，如果为 0，页单元会将线性地址存在 cr2 控制寄存器中，并产生异常号 14： page fault。</p>
<p>页表目录结构中另外有一个 Page Size 标志（页表结构没有此标志），如果设为 1，则页面大小可以为 2MB 或者 4MB，这样可以跳过页表转换，将 cr4 寄存器的 PSE 标志启用即可启用大页面支持，此时 32 位线程地址由高 10 位页表目录下标和低 22 位的偏移量。</p>
<p>为满足寻址超过 4GB 的需求，Intel 从 Pentium Pro 处理器开始，将处理器的地址引脚数量由原来的 32 个提升为 36 个，处理器的寻址空间也从 4GB 增到 64GB，并增加 PAE 页面机制（设置 cr4 寄存器的 PAE 标志启用）：64G内存可以划分为 2^24 个页面，页表中的基地址由 20 位增为 24 位，页表结构的大小由 32 位增为 64 位，增加 PDDT 表从而使用三层寻址设计来解释 32 位的线性地址等等。PAE 机制稍显复杂，而且由于仍然使用 32 位线性地址，因此对于应用程序来说，仍然无法使用超过 4GB 的地址空间，64GB 只是对于 kernel 而言的。</p>
<p>顺带说下不同的 64 位架构下的页面寻址级别，见下表，可以看到常用的 x86_64 架构只用了 48 位的线性地址空间，但也达到了 256TB 咯 ^_^</p>
<p><a href="https://zohead.com/wp-content/uploads/64bit-arch-paging-level.jpg" target="_blank"><img class="alignnone" title="64位架构的页面级别" src="https://zohead.com/wp-content/uploads/64bit-arch-paging-level.jpg" alt="64位架构的页面级别" width="438" height="108" /></a></p>
<h2 id="paging-hardware">硬件 cache</h2>
<p>由于现在 CPU 速度太快，频率已经动辄多少 GHz，而相对的 DRAM 内存频率就慢很多，而且 DRAM 由于设计上电容存在不可避免的漏电原因，DRAM 的数据只能保持很短的时间，必须隔一段时间就刷新一次，不刷新的话会造成存储的信息丢失；而 SRAM 在加电之后不需要刷新，数据也不会丢失，由于 SRAM 的内部结构明显比 DRAM 复杂，而且由于价格原因不能将容量做的很大，DRAM 常用于 PC 机的内存，而 SRAM 常用于 CPU 的 L1 和 L2、L3 缓存，这时位于 SRAM 和 DRAM 之间的处理器 cache 控制器就应运而生了。</p>
<p>首先 CPU 从 cache 里读取的数据是以数据总线宽度为单位的，而新引入的 cache line 则是cache 和 memory 之间数据传输的最小单元，一般的 cache line size 有 32个字节、64个字节等。cache memory 的大小一般以 cache line size 为单位，可以包含多个 cache line，假设 cache line size 是 32 字节，数据总线宽度是 128 位，一个 cache line 就需要多次的总线操作，为此 x86 可以使用锁总线来保证一个操作序列是原子的。</p>
<p>CPU 访问 RAM 地址时，首先会根据地址判断是否在 cache 中，假设 cache 命中，如果是读操作，cache 控制器从 cache memory 中取得数据传给 CPU 寄存器，RAM 就不被访问以提高性能，如果是写操作，cache 控制器一般都需要实现 write-through 和 write-back 两种缓存策略。对于 L1 cache，大多是 write-through 的（同时写 RAM 和 cache line）；L2 cache 则是 write-back 的，只更新 cache line，不会立即写回 memory，只在需要时再更新，而且 cache 控制器一般只在 CPU 得到需要刷新 cache line 的指令时才刷新。反之 cache 未命中时，cache line 中的数据需要写回 memory，如果需要的话，将正确的 cache line 的数据从 RAM 中取出并更新。</p>
<p>如果能提高 CPU 的 cache 命中率，减少 cache 和 memory 之间的数据传输，将会提高系统的性能。</p>
<p>在多处理器环境中，每个处理器都有独立的硬件 cache。因此存在 cache 一致性问题，需要额外的硬件电路同步 cache 内容。</p>
<p>Linux kernel 中默认对所有页面启用 cache，并且都使用 write-back 策略。</p>
<p>TLB（Translation Lookaside Buffers）的作用：</p>
<p>除了通用的硬件 cache 之外，80X86 处理器包含 TLB（Translation Lookaside Buffers）cache 用于提高线性地址转换的速度。某个地址第一次使用时，MMU 将它对应的物理地址填入 TLB 中，下次使用同一地址时就可以从 TLB cache 里取出。TLB 中的内容需要保持与页表的一致，页面目录的物理地址变化时（更新 cr3 寄存器的值），TLB 中的所有内容也会被更新。</p>
<p>另外在多处理器环境中，每个处理器都有自己的 TLB，不过多处理器下的 TLB 是不需要像 CPU cache 那样做同步，因为某个进程对于不同的处理器的线性地址是相同的。</p>
<h2 id="linux-page-management">Linux 内存分页管理</h2>
<p>2.6.11 之后的新 Linux kernel 中为了兼容 x86_64 等硬件架构已经将原先的两层页结构改为四层页结构：页全局目录（PGD）、页上级目录（PUD）、页中间目录（PMD）、页表（PT）。这样一个线性地址就被分成了 5 个部分，为了适应不同架构的考虑，这 5 个部分的位长度并没有固定，有的 64 位硬件架构只使用了三层页结构。</p>
<p>对于 32 位又没有 PAE 的架构，两层页结构就已经够了，此时 Linux 将的 PUD、PMD 的长度设为 0 位，为了使同样的代码既可以在 32 位又能在 64 位上运行，Linux 会将 PUD、PMD 的条目数设为 1，并将 PUD、PMD 映射放到 PGD 的合适条目上，PUD 的惟一的条目指向下一级的 PMD，PMD 的惟一的条目指向下一级的 PT，这样可以做到对于 OS 来说还是使用四层页结构。对于 32 位并启用了 PAE 的架构，PGD 对应于原来的 PDPT，PUD 被移除（长度为 0 位），PMD 对应于原来的 页目录，PT 还是对应于原来的页表。</p>
<p>下面这张摘来的图很好的说明了 段式内存管理 和 页式内存管理 的关系（还算简单，没有画出 PGD、PUD、PMD 这种东西）：</p>
<p><a href="https://zohead.com/wp-content/uploads/memory-segmentation-paging.jpg" target="_blank"><img class="alignnone" title="段式与页式内存管理" src="https://zohead.com/wp-content/uploads/memory-segmentation-paging.jpg" alt="段式与页式内存管理" width="465" height="376" /></a></p>
<p>每个进程有自己的 PGD 和 页表，当进程发生切换时，当前进程的描述符中就保存了 cr3 寄存器的值。</p>
<p>页表、PMD、PUD、PGD 在 Linux kernel 中分别用 pte_t、pmd_t、pud_t 和 pgd_t 来表示，PAE 启用时这些值为 64 位，否则为 32 位。Linux kernel 提供 pte_clear、set_pte、pte_none、pte_same 等宏（函数）来操作或判断页表的值。</p>
<p>pmd_present 宏根据指定的页或页表是否在内存中而返回 1 或 0。而 pud_present、pgd_present 始终返回 1。需要注意的是 pte_present 宏，当页表结构中 Present 或者 Page Size 标志位为 1 时，pte_present 都返回 1，否则返回 0。由于 Page Size 标志位对于页表没有意义，因此对于一些虽然在内存中但并没有 读/写/执行 权限的 page，kernel 会将其 Present 标志位置为 0 及 Page Size 标志位置为 1，由于 Present 被置为 0，访问这些 page 时将触发 page fault 异常，但这时 kernel 就会根据 Page Size 为 1 而判断出这个异常不是真的由缺页引起的。</p>
<p>pgd_index、pmd_index、pte_index 分别返回指定的线性地址在 PGD、PMD、PT 中映射该线性地址所在项的索引，其它还有一些例如 pte_page、pmd_page、pud_page、pgd_page 这种操作不同种类的 page descriptor 的函数（宏）。</p>
<h3 id="linux-kernel-memory-layout">Linux kernel 内存布局</h3>
<p>现在的 2.6 bzImage kernel 在启动时一般装载在 0x100000 即 1MB 的内存地址上（2.4 zImage 默认装载在 0x10000 内存地址上，具体请参考 Linux boot protocol - Documentation/x86/boot.txt），因为 1MB 之前的内存被 BIOS 和一些设备使用，这些可以找 BIOS 内存图来参考学习。</p>
<p>kernel 中有 min_low_pfn 变量表示在内存中 kernel 映像之后第一个可用页框的页号，max_pfn 表示最后一个可用页框的页号，max_low_pfn 表示最后一个由 kernel 直接映射的页框的页号（low memory），totalhigh_pages 表示 kernel 不能直接映射的页框数（high memory），highstart_pfn 和 highend_pfn 就比较好理解了。</p>
<p>在 32 位 Linux 中地址空间为4G，0~3G 为用户空间，物理地址的 0~896M 是直接写死的内核空间（即 low memory），大于 896M 的物理地址必须建立映射才能访问，可以通过 alloc_page() 等函数获得对应的 page，大于 896M 的就称为高端内存（high memory），另外剩下 896M~1G 的 128M 空间就用来映射 high memory 的，这段空间包括：</p>
<ol>
<li>
<p>noncontiguous memory area 映射</p>
<p>从 VMALLOC_OFFSET 到 VMALLOC_END，其中 VMALLOC_OFFSET 距 high memory 8MB，每个被映射的 noncontiguous memory area 中间还要间隔一个页面（4KB）；</p>
</li>
<li>
<p>映射 persistent kernel mapping</p>
<p>从 PKMAP_BASE 开始；</p>
</li>
<li>
<p>最后用于 fix-mapped linear address</p>
<p>见下面的固定映射的说明。</p>
</li>
</ol>
<p>所有进程从 3GB 到 4GB 的虚拟空间都是一样的，Linux 以此方式让内核态进程共享代码段和数据段。</p>
<p>3GB（0xC0000000）就是物理地址与虚拟地址之间的位移量，在 Linux kernel 代码中就叫做 PAGE_OFFSET。Linux kernel 提供 __pa 宏将从 PAGE_OFFSET 开始的线性地址转换为对应的物理地址，__va 则做相反的操作。</p>
<p><em>备注：Linux kernel 中有配置选项可以将 用户/内核 空间划分为分别 2G，64位 Linux 由于不存在 4G 地址空间限制不存在高端内存。</em></p>
<p>下面以一个编译 32 位 Linux kernel 时产生的 System.map 符号表文件说明下 kernel 在内存中的使用情况，在  System.map 里可以看到下面几个符号：</p>
<pre class="brush: plain; title: ; notranslate">
c1000000 T _text
c131bab6 T _etext
c131d000 R __start_rodata
c143c000 R __end_rodata
c143c000 D _sdata
c1469f40 D _edata
c151a000 B __bss_start
c1585154 B __bss_stop
c16ab000 A _end
</pre>
<p>_text 表示 kernel code 开始处，这个在符号表就被链接到 0xc0000000 + 0x100000，这样所有的符号地址 = 0xC0000000 + 符号，_etext 表示 kernel code 结束处，之后是 rodata 只读数据段的开始 __start_rodata 和结束位置 __end_rodata；已初始化的 kernel data 在 _sdata 开始并结束于 _edata 位置，紧接着是未初始化的 kernel data（BSS），开始于 __bss_start 结束于 __bss_stop，通常 System.map 的最后都是 _end。（注意：这里看到的 kernel ELF 段分布和 ULK 说的并不完全一样，ULK 说的相对比较笼统）</p>
<h3 id="linux-kernel-memory-mapping">Linux kernel 内存映射</h3>
<p>PGD 被分成了两部分，第一部分表项映射的线性地址小于 0xc0000000 （PGD 共 1024 项，在 PAE 未启用时是前 768 项，PAE 启用时是前 3 项），具体大小依赖特定进程。相反，剩余的表项对所有进程来说都应该是相同的，它们等于 master kernel PGD 的相应表项。</p>
<p>系统在初始化时，kernel 会维护一个 master kernel PGD，初始化之后，这个 master kernel PGD 将不会再被任何进程或者 kernel 线程直接使用，而对于系统中的常规进程的 PGD，最开始的一些 PGD 条目（PAE 禁用时为最开始 768 条，启用时为最开始 3 条）是进程相关的，其它的 PGD 条目则和其它进程一样统一指向对应的 master kernel PGD 中的最高的一些 PGD 条目，master kernel PGD 只相当于参考模型。</p>
<p>kernel 刚被装载到内存时，CPU 还处于实模式，分页功能还未启用，首先 kernel 会创建一个有限的 kernel code 和 data 地址空间、初始页表、以及一些动态数据；然后 kernel 利用这个最小的地址空间完成使用所有 RAM 并正确设置页表。</p>
<p>看看 arch/x86/kernel/head_32.S 中 kernel 临时页表的 AT&amp;T 汇编代码：</p>
<pre class="brush: cpp; title: arch/x86/kernel/head_32.S; notranslate">
page_pde_offset = (__PAGE_OFFSET &gt;&gt; 20);

	movl $pa(__brk_base), %edi
	movl $pa(swapper_pg_dir), %edx
	movl $PTE_IDENT_ATTR, %eax
10:
	leal PDE_IDENT_ATTR(%edi),%ecx		/* Create PDE entry */
	movl %ecx,(%edx)			/* Store identity PDE entry */
	movl %ecx,page_pde_offset(%edx)		/* Store kernel PDE entry */
	addl $4,%edx
	movl $1024, %ecx
11:
	stosl
	addl $0x1000,%eax
	loop 11b
	/*
	 * End condition: we must map up to the end + MAPPING_BEYOND_END.
	 */
	movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp
	cmpl %ebp,%eax
	jb 10b
	addl $__PAGE_OFFSET, %edi
	movl %edi, pa(_brk_end)
	shrl $12, %eax
	movl %eax, pa(max_pfn_mapped)

	/* Do early initialization of the fixmap area */
	movl $pa(swapper_pg_fixmap)+PDE_IDENT_ATTR,%eax
	movl %eax,pa(swapper_pg_dir+0xffc)

// ... 中间代码省略 ... //

/*
 * Enable paging
 */
	movl $pa(swapper_pg_dir),%eax
	movl %eax,%cr3		/* set the page table pointer.. */
	movl %cr0,%eax
	orl  $X86_CR0_PG,%eax
	movl %eax,%cr0		/* ..and set paging (PG) bit */
	ljmp $__BOOT_CS,$1f	/* Clear prefetch and normalize %eip */
1:
	/* Set up the stack pointer */
	lss stack_start,%esp
</pre>
<p>kernel 临时页表就包含在 swapper_pg_dir 中，最后通过设置 cr3 寄存器启用内存分页管理。master kernel PGD 在 paging_init 中初始化，其中调用 pagetable_init：</p>
<pre class="brush: cpp; title: arch/x86/mm/init_32.c; notranslate">
#ifdef CONFIG_HIGHMEM
static void __init permanent_kmaps_init(pgd_t *pgd_base)
{
	unsigned long vaddr;
	pgd_t *pgd;
	pud_t *pud;
	pmd_t *pmd;
	pte_t *pte;

	vaddr = PKMAP_BASE;
	page_table_range_init(vaddr, vaddr + PAGE_SIZE*LAST_PKMAP, pgd_base);

	pgd = swapper_pg_dir + pgd_index(vaddr);
	pud = pud_offset(pgd, vaddr);
	pmd = pmd_offset(pud, vaddr);
	pte = pte_offset_kernel(pmd, vaddr);
	pkmap_page_table = pte;
}

#else
static inline void permanent_kmaps_init(pgd_t *pgd_base)
{
}
#endif /* CONFIG_HIGHMEM */

static void __init pagetable_init(void)
{
	pgd_t *pgd_base = swapper_pg_dir;

	permanent_kmaps_init(pgd_base);
}

/*
 * paging_init() sets up the page tables - note that the first 8MB are
 * already mapped by head.S.
 *
 * This routines also unmaps the page at virtual kernel address 0, so
 * that we can trap those pesky NULL-reference errors in the kernel.
 */
void __init paging_init(void)
{
	pagetable_init();

	__flush_tlb_all();

	kmap_init();

	/*
	 * NOTE: at this point the bootmem allocator is fully available.
	 */
	sparse_init();
	zone_sizes_init();
}
</pre>
<p>如果计算机内存少于 896M，32 位地址就已经足够寻址所有 RAM，就不必要开启 PAE 了。如果内存多于 4GB 且 CPU 支持 PAE，kernel 也已经启用 PAE，则使用三层页结构，并使用大页面以减少页表数。</p>
<h3 id="linux-kernel-memory-fixmap">有关固定映射</h3>
<p>kernel 线性地址的 896M 映射系统物理内存，然而至少 128MB 的线性地址总是留作他用，因为内核使用这些线性地址实现 非连续内存分配 和 固定映射的线性地址。</p>
<p>Linux 内核中提供了一段虚拟地址用于固定映射，也就是 fixmap。fixmap 是这样一种机制：提供一些线性地址，在编译时就确定下来，等到 Linux 引导时再为之建立起和物理地址的映射（用 set_fixmap(idx, phys)、set_fixmap_nocache 函数）。fixmap 地址比指针更加好用，dereference 也比普通的指针速度要快，因为普通的指针 dereference 时比 fixmap 地址多一个内存访问，而且 fixmap 在 dereference 时也不需要做检查是否有效的操作。fixmap 地址可以在编译时作为一个常量，只是这个常量在 kernel 启动时被映射。</p>
<p>kernel 能确保在发生上下文切换时 fixmap 的页表项不会从 TLB 中被 flush，这样对它的访问可以始终通过高速缓存。</p>
<p>固定映射的线性地址（fix-mapped linear address）是一个固定的线性地址，它所对应的物理地址不是通过简单的线性转换得到的，而是人为强制指定的。每个固定的线性地址都映射到一块物理内存页。固定映射线性地址能够映射到任何一页物理内存。固定映射线性地址</p>
<p>固定映射线性地址是从整个线性地址空间的最后 4KB 即线性地址 0xfffff000 向低地址进行分配的。在最后 4KB 空间与固定映射线性地址空间的顶端空留一页（未知原因），固定映射线性地址空间前面的地址空间就是 vmalloc 分配的区域，他们之间也空有一页。</p>
<p>固定映射的线性地址基本上是一种类似于 0xffffc000 这样的常量线性地址，其对应的物理地址不必等于线性地址减去 0xc000000，而是通过页表以任意方式建立。因此每个固定映射的线性地址都映射一个物理内存的页框。</p>
<p>每个 fixmap 地址在 kernel 里是放在 enum fixed_addresses 数据结构中的，fix_to_virt 函数用于将 fixmap 在 fixed_addresses 中的索引转换为虚拟地址。fix_to_virt 还是一个 inline 函数，编译时不会产生函数调用代码。</p>
<h3 id="tlb-paging-hardware">处理硬件 cache 和 TLB</h3>
<p>以下措施用于优化硬件cache（L1、L2 cache 等）的命中率：</p>
<p>1) 一个数据结构中使用最频繁的字段被放在数据结构的低偏移量，这样可以在同一个 cache line 里被缓存；<br />
2) 分配很多块数据结构时，kernel 会尝试将它们分别存在 memory 中以使所有 cache line 都能被均匀使用。</p>
<p>80x86 处理器会自动处理 cache 同步，Linux kernel 只对其它没有同步 cache 操作的处理器单独做 cache flushing。</p>
<p>Linux 提供了一些在页表变化时 flush TLB 的函数，例如 flush_tlb（常用于进程切换时）、flush_tlb_all（常用于更新 kernel 页表项时） 等。</p>
<p>到此 ULK 的内存寻址部分结束，本文只是我个人看 ULK 时的认识和查找到的一些总结，这算是我所写的日志里花的时间最长的一篇（从 5月22日 写到 5月26日），花的心思也很多。本文基于 32 位 Linux kernel 而言，主要着重于 Linux 中最重要的部分之一：内存管理，本文中像 CPU L1、L2 cache 之类的一些信息都是笔者在看 ULK 不太了解时通过在网上查其它的文章而记录下的，因此里面有任何不正确之处欢迎指正 ^_^</p>
]]></content:encoded>
			<wfw:commentRss>https://zohead.com/archives/linux-kernel-learning-memory-addressing/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
