<?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; 内存</title>
	<atom:link href="https://zohead.com/archives/tag/memory/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-process-address-space/</link>
		<comments>https://zohead.com/archives/linux-kernel-learning-process-address-space/#comments</comments>
		<pubDate>Fri, 06 Jul 2012 19:12:14 +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[brk]]></category>
		<category><![CDATA[malloc]]></category>
		<category><![CDATA[mmap]]></category>
		<category><![CDATA[mm_struct]]></category>
		<category><![CDATA[VMA]]></category>
		<category><![CDATA[内存]]></category>
		<category><![CDATA[地址空间]]></category>
		<category><![CDATA[线程]]></category>
		<category><![CDATA[进程]]></category>
		<category><![CDATA[页表]]></category>

		<guid isPermaLink="false">http://zohead.com/?p=266</guid>
		<description><![CDATA[本文同步自（如浏览不正常请点击跳转）：https://zohead.com/archives/linux-kernel-learning-process-address-space/ 看完 Linux kernel block I/O 层之后来到进程地址空间管理部分，本文中的很多知识和之前的 [进程基本]、[进程调度]、[内存管理] 等章节的知识相关。 1、基础知识： Linux kernel 给每个进程提供的进程地址空间一般是 32 位或 64 位（硬件相关）的平坦地址空间，但进程是没有权限访问这段地址空间中的所有地址的，能访问的一般是很多的内存地址区间。这种内存地址区间被称为内存区域，进程 [&#8230;]]]></description>
				<content:encoded><![CDATA[<p>本文同步自（如浏览不正常请点击跳转）：<a href="https://zohead.com/archives/linux-kernel-learning-process-address-space/" target="_blank">https://zohead.com/archives/linux-kernel-learning-process-address-space/</a></p>
<p>看完 Linux kernel block I/O 层之后来到进程地址空间管理部分，本文中的很多知识和之前的 [<a href="https://zohead.com/archives/linux-kernel-learning-process/" target="_blank">进程基本</a>]、[<a href="https://zohead.com/archives/linux-kernel-learning-process-scheduling/" target="_blank">进程调度</a>]、[<a href="https://zohead.com/archives/linux-kernel-learning-memory-management/" target="_blank">内存管理</a>] 等章节的知识相关。</p>
<p><strong><span style="color: #ff0000;">1、基础知识：</span></strong></p>
<p>Linux kernel 给每个进程提供的进程地址空间一般是 32 位或 64 位（硬件相关）的平坦地址空间，但进程是没有权限访问这段地址空间中的所有地址的，能访问的一般是很多的内存地址区间。这种内存地址区间被称为内存区域，进程可以动态添加和删除内存区域到它的地址空间中。内存区域可以有不同的权限，相关进程必须遵守这些权限，例如可读、可写、可执行等。如果进程访问的地址不在一个有效的内存区域中，或者访问时的权限不正确，kernel 将会杀掉进程并给出常见的 "Segmentation Fault" 段错误日志。</p>
<p>内存区域通常包括：</p>
<ul>
<li>可执行文件的代码段，称为 text 段；</li>
<li>可执行文件的已初始化全局变量段，称为 data 段；</li>
<li>未初始化全局变量段（通常以 0 page 填充），称为 bss 段；</li>
<li>进程的用户空间栈（通常以 0 page 填充）；</li>
<li>每个共享库文件的额外 text、data、bss 段，也被装入进程的地址空间；</li>
<li>内存映射文件；</li>
<li>共享内存区域；</li>
<li>匿名内存映射（新版本的 malloc 函数就除了 brk 之外也通过 mmap 实现）；</li>
<li>应用程序中的堆</li>
</ul>
<p><strong><span style="color: #ff0000;">2、内存描述符：</span></strong></p>
<p>kernel 使用 mm_struct 内存描述符结构来表示进程的地址空间信息，它定义在 &lt;linux/mm_types.h&gt; 头文件中，这也是一个非常大的结构。</p>
<pre class="brush: cpp; title: &lt;linux/mm_types.h&gt;; notranslate">
struct vm_area_struct {
	struct mm_struct * vm_mm;	/* The address space we belong to. */
	unsigned long vm_start;		/* Our start address within vm_mm. */
	unsigned long vm_end;		/* The first byte after our end address
					   within vm_mm. */

	/* linked list of VM areas per task, sorted by address */
	struct vm_area_struct *vm_next;

	pgprot_t vm_page_prot;		/* Access permissions of this VMA. */
	unsigned long vm_flags;		/* Flags, see mm.h. */

	struct rb_node vm_rb;

	/*
	 * For areas with an address space and backing store,
	 * linkage into the address_space-&gt;i_mmap prio tree, or
	 * linkage to the list of like vmas hanging off its node, or
	 * linkage of vma in the address_space-&gt;i_mmap_nonlinear list.
	 */
	union {
		struct {
			struct list_head list;
			void *parent;	/* aligns with prio_tree_node parent */
			struct vm_area_struct *head;
		} vm_set;

		struct raw_prio_tree_node prio_tree_node;
	} shared;

	/*
	 * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
	 * list, after a COW of one of the file pages.	A MAP_SHARED vma
	 * can only be in the i_mmap tree.  An anonymous MAP_PRIVATE, stack
	 * or brk vma (with NULL file) can only be in an anon_vma list.
	 */
	struct list_head anon_vma_chain; /* Serialized by mmap_sem &amp;
					  * page_table_lock */
	struct anon_vma *anon_vma;	/* Serialized by page_table_lock */

	/* Function pointers to deal with this struct. */
	const struct vm_operations_struct *vm_ops;

	/* Information about our backing store: */
	unsigned long vm_pgoff;		/* Offset (within vm_file) in PAGE_SIZE
					   units, *not* PAGE_CACHE_SIZE */
	struct file * vm_file;		/* File we map to (can be NULL). */
	void * vm_private_data;		/* was vm_pte (shared mem) */
	unsigned long vm_truncate_count;/* truncate_count or restart_addr */

#ifndef CONFIG_MMU
	struct vm_region *vm_region;	/* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
	struct mempolicy *vm_policy;	/* NUMA policy for the VMA */
#endif
};

struct mm_struct {
	struct vm_area_struct * mmap;		/* list of VMAs */
	struct rb_root mm_rb;
	struct vm_area_struct * mmap_cache;	/* last find_vma result */
#ifdef CONFIG_MMU
	unsigned long (*get_unmapped_area) (struct file *filp,
				unsigned long addr, unsigned long len,
				unsigned long pgoff, unsigned long flags);
	void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
#endif
	unsigned long mmap_base;		/* base of mmap area */
	unsigned long task_size;		/* size of task vm space */
	unsigned long cached_hole_size; 	/* if non-zero, the largest hole below free_area_cache */
	unsigned long free_area_cache;		/* first hole of size cached_hole_size or larger */
	pgd_t * pgd;
	atomic_t mm_users;			/* How many users with user space? */
	atomic_t mm_count;			/* How many references to &quot;struct mm_struct&quot; (users count as 1) */
	int map_count;				/* number of VMAs */
	struct rw_semaphore mmap_sem;
	spinlock_t page_table_lock;		/* Protects page tables and some counters */

	struct list_head mmlist;		/* List of maybe swapped mm's.	These are globally strung
						 * together off init_mm.mmlist, and are protected
						 * by mmlist_lock
						 */

	unsigned long hiwater_rss;	/* High-watermark of RSS usage */
	unsigned long hiwater_vm;	/* High-water virtual memory usage */

	unsigned long total_vm, locked_vm, shared_vm, exec_vm;
	unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
	unsigned long start_code, end_code, start_data, end_data;
	unsigned long start_brk, brk, start_stack;
	unsigned long arg_start, arg_end, env_start, env_end;

	unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */

	/*
	 * Special counters, in some configurations protected by the
	 * page_table_lock, in other configurations by being atomic.
	 */
	struct mm_rss_stat rss_stat;

	struct linux_binfmt *binfmt;

	cpumask_t cpu_vm_mask;

	/* Architecture-specific MM context */
	mm_context_t context;

	/* Swap token stuff */
	/*
	 * Last value of global fault stamp as seen by this process.
	 * In other words, this value gives an indication of how long
	 * it has been since this task got the token.
	 * Look at mm/thrash.c
	 */
	unsigned int faultstamp;
	unsigned int token_priority;
	unsigned int last_interval;

	unsigned long flags; /* Must use atomic bitops to access the bits */

	struct core_state *core_state; /* coredumping support */
#ifdef CONFIG_AIO
	spinlock_t		ioctx_lock;
	struct hlist_head	ioctx_list;
#endif
#ifdef CONFIG_MM_OWNER
	/*
	 * &quot;owner&quot; points to a task that is regarded as the canonical
	 * user/owner of this mm. All of the following must be true in
	 * order for it to be changed:
	 *
	 * current == mm-&gt;owner
	 * current-&gt;mm != mm
	 * new_owner-&gt;mm == mm
	 * new_owner-&gt;alloc_lock is held
	 */
	struct task_struct *owner;
#endif

#ifdef CONFIG_PROC_FS
	/* store ref to file /proc/&lt;pid&gt;/exe symlink points to */
	struct file *exe_file;
	unsigned long num_exe_file_vmas;
#endif
#ifdef CONFIG_MMU_NOTIFIER
	struct mmu_notifier_mm *mmu_notifier_mm;
#endif
};
</pre>
<p>结构的注释中已经包含比较多的注解了哦。mmap 为地址空间的内存区域（用 vm_area_struct 结构来表示啦，也是上面的代码中）链表，mm_rb 则将其以红黑树的形式进行存储，链表形式方便遍历，红黑树形式方便查找。mm_users 为以原子变量形式保护的使用此地址空间的进程数量值（例如：如果有 4 个线程共享此地址空间，则 mm_users 值为 4），mm_count 为引用计数（所有 mm_users 等于一个引用计数），当 mm_count 值为 0 时表示没有再被使用，可以被释放。total_vm 成员表示所有内存区域的数量。</p>
<p>所有的 mm_struct 结构以链表的形式存在 mm_struct 的 mmlist 成员中，该链表的第一个成员就是 init 进程的 mm_struct ：init_mm，该链表被 mmlist_lock 锁保护。</p>
<p>进程的内存描述符是在 task_struct 的 mm 成员中的。fork() 进行创建进程时调用 copy_mm 函数将父进程的内存描述符拷贝给子进程，调用 clone() 函数时如果指定 CLONE_VM 参数将使父进程和子进程地址空间共享（实际上将 mm_users 计数加 1），这种子进程就被称为线程。mm_struct 结构一般是通过 alloc_mm 宏从名为 mm_cachep 的 Slab cache 中分配。</p>
<p>进程退出时调用 exit_mm 函数，该函数再调用 mmput() 函数，此函数中减小地址空间的 mm_users 计数，如果 mm_users 变为 0，调用 mmdrop() 函数减小 mm_count 计数，如果 mm_count 变为 0，则最终调用 free_mm() 宏来释放内存描述符（回归到 Slab cache 中）。</p>
<p>另外需要说明的是 kernel 线程是没有地址空间，也就没有对应的 mm_struct（值为 NULL），kernel 线程使用之前运行的进程的内存描述符，有关 kernel 线程请参考之前的 [<a href="https://zohead.com/archives/linux-kernel-learning-process/" target="_blank">进程基本</a>] 文章。</p>
<p><strong><span style="color: #ff0000;">3、VMA 概念：</span></strong></p>
<p>vm_area_struct 结构即内存区域常被称为虚拟内存区域（简写为 VMA），表示的是在一个地址空间中的一个连续内存地址区间，每个内存区域是一个惟一的对象。vm_area_struct 中的 vm_mm 成员指向关联的内存描述符，vm_ops 成员为非常重要的关联的操作函数结构，vm_start 为起始地址，vm_end 为结束地址之后第一个字节的地址，即地址范围为：[vm_start, vm_end)。每个 VMA 对于它关联的内存描述符来说是惟一的，因此如果两个单独的进程映射相同的文件到各自的地址空间，它们的 VMA 也是不同的。</p>
<p>VMA 中的 vm_flags 表示内存区域中的页的行为状态，常见的状态有：VM_READ（页可读）、VM_WRITE（页可写）、VM_EXEC（页可被执行）、VM_SHARED（页被共享，被设置了称为共享映射，未设置称为私有映射）、VM_SHM（此区域被用作共享内存）、VM_LOCKED（页被锁）、VM_IO（此区域用于映射设备 I/O 空间）、VM_RESERVED（表示内存区域不可被交换出去）、VM_SEQ_READ（连续读，增强 readahead）、VM_RAND_READ（随机读，减弱 readahead）等。VM_SEQ_READ 和 VM_RAND_READ 标志可以通过 madvise() 系统调用来设置。</p>
<p>看看 vm_ops 操作函数结构的 vm_operations_struct 的定义，它在 &lt;linux/mm.h&gt; 头文件中：</p>
<pre class="brush: cpp; title: &lt;linux/mm.h&gt;; notranslate">
struct vm_operations_struct {
	void (*open)(struct vm_area_struct * area);
	void (*close)(struct vm_area_struct * area);
	int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);

	/* notification that a previously read-only page is about to become
	 * writable, if an error is returned it will cause a SIGBUS */
	int (*page_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);

	/* called by access_process_vm when get_user_pages() fails, typically
	 * for use by special VMAs that can switch between memory and hardware
	 */
	int (*access)(struct vm_area_struct *vma, unsigned long addr,
		      void *buf, int len, int write);
#ifdef CONFIG_NUMA
	/*
	 * set_policy() op must add a reference to any non-NULL @new mempolicy
	 * to hold the policy upon return.  Caller should pass NULL @new to
	 * remove a policy and fall back to surrounding context--i.e. do not
	 * install a MPOL_DEFAULT policy, nor the task or system default
	 * mempolicy.
	 */
	int (*set_policy)(struct vm_area_struct *vma, struct mempolicy *new);

	/*
	 * get_policy() op must add reference [mpol_get()] to any policy at
	 * (vma,addr) marked as MPOL_SHARED.  The shared policy infrastructure
	 * in mm/mempolicy.c will do this automatically.
	 * get_policy() must NOT add a ref if the policy at (vma,addr) is not
	 * marked as MPOL_SHARED. vma policies are protected by the mmap_sem.
	 * If no [shared/vma] mempolicy exists at the addr, get_policy() op
	 * must return NULL--i.e., do not &quot;fallback&quot; to task or system default
	 * policy.
	 */
	struct mempolicy *(*get_policy)(struct vm_area_struct *vma,
					unsigned long addr);
	int (*migrate)(struct vm_area_struct *vma, const nodemask_t *from,
		const nodemask_t *to, unsigned long flags);
#endif
};
</pre>
<p>当指定的内存区域被添加到地址空间时，open 函数被调用，反之移除时 close 函数被调用。如果一个不在内存中的页被访问，将触发缺页异常， fault 函数被缺页异常处理函数调用。当一个只读的页变为可写的时候，page_mkwrite 函数也被缺页异常处理函数调用。</p>
<p>mm_struct 中的 mmap 为内存区域链表，通过 VMA 的 vm_next 成员指向下一个内存区域，而且链表中的内存区域是按地址上升排序的，链表中最后一个 VMA 值为 NULL。而对于 mm_struct 的 mm_rb 红黑树，mm_rb 为红黑树的根，每个 VMA 通过其 vm_rb 红黑树节点类型链到红黑树中。</p>
<p>在应用层中可以通过 cat /proc/&lt;pid&gt;/maps 或者 pmap 程序等方法查看应用程序的内存区域列表。</p>
<p><strong>操作 VMA：</strong></p>
<p>kernel 提供 find_vma() 函数用于查找指定的内存地址在哪个 VMA 上，它的实现在 mm/mmap.c 文件中，输入参数为内存描述符和内存地址：</p>
<pre class="brush: cpp; title: mm/mmap.c; notranslate">
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
{
	struct vm_area_struct *vma = NULL;

	if (mm) {
		/* Check the cache first. */
		/* (Cache hit rate is typically around 35%.) */
		vma = mm-&gt;mmap_cache;
		if (!(vma &amp;&amp; vma-&gt;vm_end &gt; addr &amp;&amp; vma-&gt;vm_start &lt;= addr)) {
			struct rb_node * rb_node;

			rb_node = mm-&gt;mm_rb.rb_node;
			vma = NULL;

			while (rb_node) {
				struct vm_area_struct * vma_tmp;

				vma_tmp = rb_entry(rb_node,
						struct vm_area_struct, vm_rb);

				if (vma_tmp-&gt;vm_end &gt; addr) {
					vma = vma_tmp;
					if (vma_tmp-&gt;vm_start &lt;= addr)
						break;
					rb_node = rb_node-&gt;rb_left;
				} else
					rb_node = rb_node-&gt;rb_right;
			}
			if (vma)
				mm-&gt;mmap_cache = vma;
		}
	}
	return vma;
}
</pre>
<p>如果找不到对应的 VMA 则返回 NULL。需要注意的是返回的 VMA 的开始地址可能比指定的内存地址大。find_vma() 函数返回的结果会被缓存到内存描述符的 mmap_cache 成员中用于提高之后的查找性能，因为后续的操作很可能还是在同样的 VMA 上。如果在 mmap_cache 中找不到则通过红黑树进行查找。</p>
<p>find_vma_prev() 函数与 find_vma() 函数类似，不过它也会返回指定地址之前的最后一个 VMA：</p>
<p><span style="color: #008000;">struct vm_area_struct * find_vma_prev(struct mm_struct *mm, unsigned long addr,<br />
struct vm_area_struct **pprev)</span></p>
<p>kernel 另外还提供了 find_vma_intersection() 函数返回符合 find_vma() 的条件并且其开始地址不在指定内存结束地址之后的 VMA。</p>
<p><strong><span style="color: #ff0000;">4、mmap 和 munmap：</span></strong></p>
<p>kernel 提供 do_mmap() 函数创建新的线性地址区间，这是用户层 mmap() 函数的底层实现，它用于将一段地址区间添加到进程的地址空间中。</p>
<p><span style="color: #008000;">unsigned long do_mmap(struct file *file, unsigned long addr, </span><span style="color: #008000;">unsigned long len,<br />
unsigned long prot, </span><span style="color: #008000;">unsigned long flag, unsigned long offset)</span></p>
<p>do_mmap 映射 file 参数指定的文件，并最终返回新创建的地址区间的初始地址。</p>
<p>offset 和 len 指定偏移量和长度。如果 file 为 NULL 并且 offset 为 0 则表示该映射后端不是基于文件的，这种映射被称为匿名映射，否则被称为基于文件的映射。prot 参数指定内存区域中页的访问权限，值可以为：PROT_READ（对应 VM_READ）、PROT_WRITE、PROT_EXEC、PROT_NONE 等。flag 指定 VMA 的其它标志，常用的有：MAP_SHARED（此映射可被共享）、MAP_PRIVATE（私有不可共享）、MAP_ANONYMOUS（指定匿名映射）、MAP_LOCKED 等。</p>
<p>如果可能的话，do_mmap 返回的内存区间会尽量和已有邻近的 VMA 合并（调整 VMA 大小），否则就创建一个新的 VMA。新的 VMA 从名为 vm_area_cachep 的 Slab cache 中分配，并通过 vma_link() 函数被加入到进程地址空间的链表和红黑树中，对应的 mm_struct 的 total_vm 成员也被更新。</p>
<p>do_mmap 是调用 do_mmap_pgoff() 函数完成真正的映射操作的。现在用户层使用的 mmap() 函数实际上是在用户层调用 mmap2() 系统调用并最终通过 do_mmap 来实现的。</p>
<p>do_munmap 用于从地址空间移除指定的地址区间：</p>
<p><span style="color: #008000;">int do_munmap(struct mm_struct *mm, unsigned long start, size_t len)</span></p>
<p>do_munmap 导出给用户层就是 munmap() 函数了。</p>
<p><strong><span style="color: #ff0000;">5、页表及应用程序 VMA：</span></strong></p>
<p>Linux kernel 使用页式内存管理，应用程序给出的内存地址是虚拟地址，它需要经过若干级页表一级一级的变换，才变成真正的物理地址。有关 Linux 的分级页表结构等相关的知识请参考之前的 [<a href="https://zohead.com/archives/linux-kernel-learning-memory-addressing/" target="_blank">内存寻址</a>] 文章。</p>
<p>每个进程有自己的 task_struct，task_struct 中的 mm 指向其内存描述符，每个 mm 又有自己单独的页表（进程中的线程会进行共享），本文最上面介绍的内存描述符 mm_struct 中的 pgd_t * pgd 就指向进程的 PGD，对页表的操作和遍历等操作也需要用到 mm_struct 中的 page_table_lock 自旋锁成员。</p>
<p>应用程序中对内存的操作例如 malloc 分配内存等一般是改变了某个 VMA，不会直接改变页表。假设用户分配了内存，然后访问这块内存，由于页表里面并没有记录相关的映射，CPU 产生一次缺页异常，内核捕捉到异常，检查产生异常的地址是不是存在于一个合法的 VMA 中，如果不是，则给进程一个 "Segmentation Fault" 段错误，使其崩溃；如果是，则分配一个物理页，并为之建立映射。</p>
<p>应用程序中的堆是一个一端固定、一端可伸缩的 VMA，其大小可以通过 brk 系统调用进行调整，libc 的 malloc 函数就是基于 brk 来实现的（如果需要分配的内存很大时，libc 会通过 mmap 系统调用映射一个新的 VMA 以节省对堆 VMA 的一系列调整操作）。应用程序的栈也是一个 VMA，只是它是一端固定、一端可伸不能缩的，而且它是自动伸展的。另外需要说明的是线程的栈 VMA 明显不是和其它线程共享的，一般是在线程创建时通过 mmap 创建新的 VMA 并以此作为线程的栈。</p>
<p>本文只是对 Linux kernel 的进程地址空间的基础涉及，其中有任何问题，欢迎提出指正哦，玩的开心~~~ ^_^</p>
]]></content:encoded>
			<wfw:commentRss>https://zohead.com/archives/linux-kernel-learning-process-address-space/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<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>
		<item>
		<title>page cache诊断控制工具 vmtouch 源代码分析</title>
		<link>https://zohead.com/archives/vmtouch-code-analysis/</link>
		<comments>https://zohead.com/archives/vmtouch-code-analysis/#comments</comments>
		<pubDate>Thu, 17 May 2012 16:29:51 +0000</pubDate>
		<dc:creator><![CDATA[Uranus Zhou]]></dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[代码分析]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[fadvise]]></category>
		<category><![CDATA[kernel]]></category>
		<category><![CDATA[mincore]]></category>
		<category><![CDATA[mlock]]></category>
		<category><![CDATA[mmap]]></category>
		<category><![CDATA[page cache]]></category>
		<category><![CDATA[posix_fadvise]]></category>
		<category><![CDATA[vmtouch]]></category>
		<category><![CDATA[内存]]></category>

		<guid isPermaLink="false">http://zohead.com/?p=166</guid>
		<description><![CDATA[本文同步自（如浏览不正常请点击跳转）：https://zohead.com/archives/vmtouch-code-analysis/ vmtouch 是一个 portable 的 page cache 诊断和控制工具，可以查看文件或者设备中有多少在 page cache 中，知道之后对这些在 page cache 中的内存引用可以避免 page fault，支持将文件的内容从 page cache 逐出，同时还可以将文件手工 touch 到 page cache 中，支持 lock 文件部分区域到 memory 中防止被交换出去从而提高。 vmtouch 可以在 Linux、BSD 等系 [&#8230;]]]></description>
				<content:encoded><![CDATA[<p>本文同步自（如浏览不正常请点击跳转）：<a href="https://zohead.com/archives/vmtouch-code-analysis/" target="_blank">https://zohead.com/archives/vmtouch-code-analysis/</a></p>
<p>vmtouch 是一个 portable 的 page cache 诊断和控制工具，可以查看文件或者设备中有多少在 page cache 中，知道之后对这些在 page cache 中的内存引用可以避免 page fault，支持将文件的内容从 page cache 逐出，同时还可以将文件手工 touch 到 page cache 中，支持 lock 文件部分区域到 memory 中防止被交换出去从而提高。</p>
<p>vmtouch 可以在 Linux、BSD 等系统上使用，在这下载编译：</p>
<p><a href="http://hoytech.com/vmtouch/" target="_blank">http://hoytech.com/vmtouch/</a></p>
<p>今天简单看了下 vmtouch 的代码，发现还比较简单，自己写个类似的程序验证之后，将代码分析结果写下。vmtouch 的代码比较少，我只贴出最关键的一个函数 vmtouch_file（关键部分已经高亮显示），这个函数做 分析 page cache 使用、touch、lock 的操作，其它部分只是加了读了目录的遍历处理之类的。</p>
<pre class="brush: cpp; highlight: [2,10,57,75,78,89,91,105,126]; title: vmtouch.c; notranslate">
int64_t bytes2pages(int64_t bytes) {
	return (bytes+pagesize-1) / pagesize;
}

int aligned_p(void *p) {
	return 0 == ((long)p &amp; (pagesize-1));
}

int is_mincore_page_resident(char p) {
	return p &amp; 0x1;
}

void vmtouch_file(char *path) {
	int fd;
	void *mem;
	struct stat sb;
	int64_t len_of_file;
	int64_t pages_in_file;
	int i;
	int res;

	res = o_followsymlinks ? stat(path, &amp;sb) : lstat(path, &amp;sb);

	if (res) {
		warning(&quot;unable to stat %s (%s), skipping&quot;, path, strerror(errno));
		return;
	}

	if (S_ISLNK(sb.st_mode)) {
		warning(&quot;not following symbolic link %s&quot;, path);
		return;
	}

	if (sb.st_size == 0) return;

	if (sb.st_size &gt; o_max_file_size) {
		warning(&quot;file %s too large, skipping&quot;, path);
		return;
	}

	len_of_file = sb.st_size;

	retry_open:

	fd = open(path, O_RDONLY, 0);

	if (fd == -1) {
		if (errno == ENFILE || errno == EMFILE) {
			increment_nofile_rlimit();
			goto retry_open;
		}

		warning(&quot;unable to open %s (%s), skipping&quot;, path, strerror(errno));
		return;
	}

	mem = mmap(NULL, len_of_file, PROT_READ, MAP_SHARED, fd, 0);

	if (mem == MAP_FAILED) {
		warning(&quot;unable to mmap file %s (%s), skipping&quot;, path, strerror(errno));
		close(fd);
		return;
	}

	if (!aligned_p(mem)) fatal(&quot;mmap(%s) wasn't page aligned&quot;, path);

	pages_in_file = bytes2pages(len_of_file);

	total_pages += pages_in_file;

	if (o_evict) {
		if (o_verbose) printf(&quot;Evicting %s\n&quot;, path);

#if defined(__linux__)
		if (posix_fadvise(fd, 0, len_of_file, POSIX_FADV_DONTNEED))
			warning(&quot;unable to posix_fadvise file %s (%s)&quot;, path, strerror(errno));
#elif defined(__FreeBSD__) || defined(__sun__)
		if (msync(mem, len_of_file, MS_INVALIDATE))
			warning(&quot;unable to msync invalidate file %s (%s)&quot;, path, strerror(errno));
#else
		fatal(&quot;cache eviction not (yet?) supported on this platform&quot;);
#endif
	} else {
		char mincore_array[pages_in_file];
		int64_t pages_in_core=0;
		double last_chart_print_time=0.0, temp_time;

		// 3rd arg to mincore is char* on BSD and unsigned char* on linux
		if (mincore(mem, len_of_file, (void*)mincore_array)) fatal(&quot;mincore %s (%s)&quot;, path, strerror(errno));
		for (i=0; i&lt;pages_in_file; i++) {
			if (is_mincore_page_resident(mincore_array[i])) {
				pages_in_core++;
				total_pages_in_core++;
			}
		}

		if (o_verbose) {
			printf(&quot;%s\n&quot;, path);
			last_chart_print_time = gettimeofday_as_double();
			print_page_residency_chart(stdout, mincore_array, pages_in_file);
		}

		if (o_touch) {
			for (i=0; i&lt;pages_in_file; i++) {
				junk_counter += ((char*)mem)[i*pagesize];
				mincore_array[i] = 1;

				if (o_verbose) {
					temp_time = gettimeofday_as_double();

					if (temp_time &gt; (last_chart_print_time+CHART_UPDATE_INTERVAL)) {
						last_chart_print_time = temp_time;
						print_page_residency_chart(stdout, mincore_array, pages_in_file);
					}
				}
			}
		}

		if (o_verbose) {
			print_page_residency_chart(stdout, mincore_array, pages_in_file);
			printf(&quot;\n&quot;);
		}
	}

	if (o_lock) {
		if (mlock(mem, len_of_file))
			fatal(&quot;mlock: %s (%s)&quot;, path, strerror(errno));
	}

	if (!o_lock &amp;&amp; !o_lockall) {
		if (munmap(mem, len_of_file)) warning(&quot;unable to munmap file %s (%s)&quot;, path, strerror(errno));
		close(fd);
	}
}
</pre>
<p>稍微有点基础就可以看明白了，先 mmap 映射文件到当前进程，按 page size 对齐之后，调用 mincore 函数就可以得到文件中每一个 page 是否在 page cache 中，结果保存在 mincore_array 数组中，该数据中每个字节的第一位即表示是否在 page cache 中。</p>
<p>将文件内容逐出（指定 o_evict）出 page cache 是通过 posix_fadvise 函数调用 fadvise 系统调用来实现的（BSD通过 msync 实现，这个在 Linux 上没有效果）。fadvise 系统调用可以告诉 kernel 要操作的文件在接下来要干什么，kernel 可以提前做一些操作而提高性能，Linux kernel 里实现了以下几种控制方式：</p>
<p>POSIX_FADV_NORMAL - 正常操作，对文件使用底层设备的默认 readahead 值；<br />
POSIX_FADV_SEQUENTIAL - 顺序I/O，对文件使用两倍的 readahead 值；<br />
POSIX_FADV_RANDOM - 随机I/O，禁用文件上的 readahead；<br />
POSIX_FADV_NOREUSE - 只使用一次<br />
POSIX_FADV_WILLNEED - 很快需要使用，对文件使用非阻塞读到 page cache<br />
POSIX_FADV_DONTNEED - 不再需要使用文件，从 page cache 中逐出</p>
<p>posix_fadvise 加 POSIX_FADV_DONTNEED 参数就可以将文件从 page cache 中逐出，需要注意的是如果需要确保文件从 page cache 中逐出，还需要在调用 fadvise 之前用 fsync/fdatasync/sync_file_range 之类的函数将 dirty page 清理。</p>
<p>下面是我在 Linux 下用 posix_fadvise 的一个测试程序测试的结果：</p>
<pre class="brush: bash; highlight: [4,7,13,16,20]; title: fadvise测试; notranslate">
[root@localhost ~]# echo 3 &gt; /proc/sys/vm/drop_caches
[root@localhost ~]# free
total       used       free     shared    buffers     cached
Mem:        374092      61832     312260          0        136       5060
-/+ buffers/cache:      56636     317456
Swap:       707576        436     707140
[root@localhost ~]# dd if=/dev/zero of=test bs=1024k count=100
记录了100+0 的读入
记录了100+0 的写出
104857600字节(105 MB)已复制，22.5514 秒，4.6 MB/秒
[root@localhost ~]# free
total       used       free     shared    buffers     cached
Mem:        374092     168960     205132          0        564     109816
-/+ buffers/cache:      58580     315512
Swap:       707576        436     707140
[root@localhost ~]# ./fadvise test POSIX_FADV_DONTNEED
OK
[root@localhost ~]# free
total       used       free     shared    buffers     cached
Mem:        374092      63932     310160          0        580       7424
-/+ buffers/cache:      55928     318164
Swap:       707576        436     707140
</pre>
<p>从 free 命令的结果可以很明显的看到，dd 之后基本文件都在 page cache 中，fadvise 之后从 page cache 中正确逐出。</p>
<p>接着是 vmtouch 中的 touch 操作（指定 o_touch）就更简单了，对 mmap 到的地址直接遍历引用，不在 page cache 的内容会自动产生 page fault 到 page cache 中。</p>
<p>lock 内存（指定 o_lock）也则直接使用 mlock 函数来实现，mlock 对于对安全性和实时性有很高要求的程序非常有用，可以保证指定的文件区域在内存中，不被 swap 出去。</p>
<p>以上为个人分析结果，有任何问题欢迎指正咯 ^_^</p>
]]></content:encoded>
			<wfw:commentRss>https://zohead.com/archives/vmtouch-code-analysis/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
