Linux Kernel - Process Address Space
Table of Contents
1. 进程地址空间
内核除了管理物理内存外,还必须管理用户空间中进程的内存。我们称这个内存为进程地址空间(Process Address Space),也就是系统中每个用户空间进程所看到的内存。Linux 操作系统采用虚拟内存技术,因此,系统中的所有进程之间以虚拟方式共享内存。对一个进程而言,它好像都可以访问整个系统的所有物理内存。更重要的是,即使单独一个进程,它拥有的地址空间也可以远远大于系统物理内存。
一些操作系统提供了段地址空间,这种地址空间并非是一个独立的线性区域,而是被分段的,但现代采用虚拟内存的操作系统通常都使用平坦地址空间而不是分段式的内存模式。通常情况下,每个进程都有唯一的这种平坦地址空间。一个进程的地址空间与另一个进程的地址空间即使有相同的内存地址,实际上也彼此互不相干。
内存地址是一个给定的值,它要在地址空间范围之内,比如 4021000。这个值表示的是进程 32 位地址空间中的一个特定的字节。尽管一个进程可以寻址 4GB 的虚拟内存(在 32 位的地址空间中),但这并不代表它就有权访问所有的虚拟地址。 在地址空间中,我们更为关心的是一些虚拟内存的地址区间,比如 08048000-804c000,它们可被进程访问。这些可被访问的合法地址空间称为内存区域(memory areas)。通过内核,进程可以给自己的地址空间动态地添加或减少内存区域。
进程只能访问有效内存区域内的内存地址。 每个内存区域也具有相关权限如对相关进程有可读、可写、可执行属性。 如果一个进程访问了不在有效范围中的内存区域,或以不正确的方式访问了有效地址,那么内核就会终止该进程,并返回“段错误”信息。
内存区域可以包含各种内存对象,比如:
- 可执行文件代码的内存映射,称为代码段(text section);
- 可执行文件的已初始化全局变量的内存映射,称为数据段(data section);
- 包含未初始化全局变量,也就是 bss 段的零页(页面中的信息全部为 0 值,所以可用于映射 bss 段等目的)的内存映射;
- 用于进程用户空间栈(不要和进程内核栈混淆,进程的内核栈独立存在并由内核维护)的零页的内存映射;
- 每一个诸如 C 库或动态连接程序等共享库的代码段、数据段和 bss 也会被载入进程的地址空间;
- 任何内存映射文件;
- 任何共享内存段;
- 任何匿名的内存映射,比如由 malloc 分配的内存。
进程地址空间中的任何有效地址都只能位于唯一的区域,这些内存区域不能相互覆盖。可以看到,在执行的进程中,每个不同的内存片段都对应一个独立的内存区域:栈、对象代码、全局变量、被映射的文件等。
2. 内存描述符
内核使用内存描述符结构体表示进程的地址空间,该结构包含了和进程地址空间有关的全部信息。内存描述符由 mm_struct
结构体表示,定义在文件<linux/sched.h>中。下面给出内存描述符的结构和各个域的描述:
struct mm_struct { struct vm_area_struct *mmap; /* list of memory areas */ struct rb_root mm_rb; /* red-black tree of VMAs */ struct vm_area_struct *mmap_cache; /* last used memory area */ unsigned long free_area_cache; /* 1st address space hole */ pgd_t *pgd; /* page global directory */ atomic_t mm_users; /* address space users */ atomic_t mm_count; /* primary usage counter */ int map_count; /* number of memory areas */ struct rw_semaphore mmap_sem; /* memory area semaphore */ spinlock_t page_table_lock; /* page table lock */ struct list_head mmlist; /* list of all mm_structs */ unsigned long start_code; /* start address of code */ unsigned long end_code; /* final address of code */ unsigned long start_data; /* start address of data */ unsigned long end_data; /* final address of data */ unsigned long start_brk; /* start address of heap */ unsigned long brk; /* final address of heap */ unsigned long start_stack; /* start address of stack */ unsigned long arg_start; /* start of arguments */ unsigned long arg_end; /* end of arguments */ unsigned long env_start; /* start of environment */ unsigned long env_end; /* end of environment */ unsigned long rss; /* pages allocated */ unsigned long total_vm; /* total number of pages */ unsigned long locked_vm; /* number of locked pages */ unsigned long saved_auxv[AT_VECTOR_SIZE]; /* saved auxv */ cpumask_t cpu_vm_mask; /* lazy TLB switch mask */ mm_context_t context; /* arch-specific data */ unsigned long flags; /* status flags */ int core_waiters; /* thread core dump waiters */ struct core_state *core_state; /* core dump support */ spinlock_t ioctx_lock; /* AIO I/O list lock */ struct hlist_head ioctx_list; /* AIO I/O list */ };
mm_users
域记录正在使用该地址的进程数目。比如,如果两个线程共享该地址空间,那么 mm_users
的值便等于 2。
mmap
域和 mm_rb
域这两个不同数据结构体描述的对象是相同的:该地址空间中的全部内存区域。但是前者以链表形式存放而后者以红黑树的形式存放。内核通常会避免使用两种数据结构组织同一种数据,但此处内核这样的冗余确实派得上用场。 mmap
结构体作为链表,利于简单、高效地遍历所有元素;而 mm_rb
结构体作为红黑树,更适合搜索指定元素。
2.1. 分配内存描述符
在进程的进程描述符(在<linux/sched.h>中定义的 task_struct
结构体就表示进程描述符)中, mm
域存放着该进程使用的内存描述符,所以 current->mm
便指向当前进程的内存描述符。 fork()
函数利用 copy_mm()
函数复制父进程的内存描述符,也就是 current->mm
域给其父进程,而子进程中的 mm_struct
结构体实际是通过文件 kernel/fork.c 中的 allocate_mm()
宏从 mm_cachep
slab 缓存中分配得到的。通常,每个进程都有唯一的 mm_struct
结构体,即唯一的进程地址空间。
如果父进程希望和其子进程共享地址空间,可以在调用 clone()
时,设置 CLONE_VM
标志。我们把这样的进程称作线程。是否共享地址空间几乎是进程和 Linux 中所谓的线程间本质上的唯一区别。除此以外,Linux 内核并不区别对待它们,线程对内核来说仅仅是一个共享特定资源的进程而已。
当 CLONE_VM
被指定后,内核就不再需要调用 allocate_mm()
函数了,而仅仅需要在调用 copy_mm 函数中将 mm 域指向其父进程的内存描述符就可以了:
if (clone_flags & CLONE_VM) { /* * current 是父进程 * tsk 在 fork() 执行期间是子进程 */ atomic_inc(¤t->mm->mm_users); tsk->mm = current->mm; }
2.2. 撤销内存描述符
当进程退出时,内核会调用定义在 kernel/exit.c 中的 exit_mm()
函数,该函数执行一些常规的撤销工作,同时更新一些统计量。其中,该函数会调用 mmput()
函数减少内存描述符中的 mm_users
用户计数,如果用户计数降到零,将调用 mmdrop()
函数,减少 mm_count
使用计数。如果使用计数也等于零了,说明该内存描述符不再有任何使用者了,那么调用 free_mm()
宏通过 kmem_cache_free()
函数将 mm_struct
结构体归还到 mm_cachep
slab 缓存中。
2.3. mm_struct 与内核线程
内核线程没有进程地址空间,也没有相关的内存描述符。所以内核线程对应的进程描述符中 mm
域为 NULL。事实上,这也正是内核线程的真实含义它们没有用户上下文。
3. 虚拟内存区域
内存区域由 vm_area_struct 结构体描述,定义在文件<linux/mm_types.h>中。内存区域在 Linux 内核中也经常称作虚拟内存区域(Virtual Memory Areas,VMAs)。
vm_area_struct
结构体描述了指定地址空间内连续区间上的一个独立内存范围。 内核将每个内存区域作为一个单独的内存对象管理,每个内存区域都拥有一致的属性,比如访问权限等 ,另外,相应的操作也都一致。按照这样的方式,每一个 VMA 就可以代表不同类型的内存区域(比如内存映射文件或者进程用户空间栈),下面给出该结构定义和各个域的描述:
struct vm_area_struct { struct mm_struct *vm_mm; /* associated mm_struct */ unsigned long vm_start; /* VMA start, inclusive */ unsigned long vm_end; /* VMA end , exclusive */ struct vm_area_struct *vm_next; /* list of VMA’s */ pgprot_t vm_page_prot; /* access permissions */ unsigned long vm_flags; /* flags */ struct rb_node vm_rb; /* VMA’s node in the tree */ union { /* links to address_space->i_mmap or i_mmap_nonlinear */ struct { struct list_head list; void *parent; struct vm_area_struct *head; } vm_set; struct prio_tree_node prio_tree_node; } shared; struct list_head anon_vma_node; /* anon_vma entry */ struct anon_vma *anon_vma; /* anonymous VMA object */ struct vm_operations_struct *vm_ops; /* associated ops */ unsigned long vm_pgoff; /* offset within file */ struct file *vm_file; /* mapped file, if any */ void *vm_private_data; /* private data */ };
每个内存描述符都对应于进程地址空间中的唯一区间。 vm_start
域指向区间的首地址(最低地址), vm_end
域指向区间的尾地址(最高地址)之后的第一个字节,也就是说,内存区域的位置就在 [vm_start,vm_end] 之中。注意,在同一个地址空间内的不同内存区间不能重叠。
vm_mm
域指向和 VMA 相关的 mm_struct
结构体,注意,每个 VMA 对其相关的 mm_struct
结构体来说都是唯一的,所以即使两个独立的进程将同一个文件映射到各自的地址空间,它们分别都会有一个 vm_area_struct
结构体来标志自己的内存区域;反过来,如果两个线程共享一个地址空间,那么它们也同时共享其中的所有 vm_area_struct
结构体。
3.1. VMA 标志
VMA 标志是一种位标志,其定义见<linux/mm.h>。它包含在 vm_flags
域内,标志了内存区域所包含的页面的行为和信息。和物理页的访问权限不同,VMA 标志反映了内核处理页面所需要遵守的行为准则,而不是硬件要求。而且, vm_flags
同时也包含了内存区域中每个页面的信息,或内存区域的整体信息,而不是具体的独立页面。表 1 列出了所有 VMA 标志的可能取值。
标志 | 对 VMA 及其页面的影响 |
---|---|
VM_READ | 页面可读取 |
VM_WRITE | 页面可写 |
VM_EXEC | 页面可执行 |
VM_SHARED | 页面可共享 |
VM_MAYREAD | VM_READ 标志可被设置 |
VM_MAYWRITE | VM_WRTE 标志可被设置 |
VM_MAYEXEC | VM_EXEC 标志可被设置 |
VM_MAYSHARE | VM_SHARE 标志可被设置 |
VM_GROWSDOWN | 区域可向下增长 |
VM_GROWSUP | 区域可向上增长 |
VM_SHM | 区域可用作共享内存 |
VM_DENYWRITE | 区域映射一个不可写文件 |
VM_EXECUTABLE | 区域映射一个可执行文件 |
VM_LOCKED | 区域中的页面被锁定 |
VM_IO | 区域映射设备 I/O 空间 |
VM_SEO_READ | 页面可能会被连续访问 |
VM_RAND_READ | 页面可能会被随机访问 |
VM_DONTCOPY | 区域不能在 fork() 时被拷贝 |
VM_DONTEXPAND | 区域不能通过 mremap() 增加 |
VM_RESERVED | 区域不能被换出 |
VM_ACCOUNT | 该区域是一个记账 VM 对象 |
VM_HUGETLB | 区域使用了 hugetlb 页面 |
VM_NONLINEAR | 该区域是非线性映射的 |
让我们进一步看看其中有趣和重要的几种标志,VM_READ、VM_WRITE 和 VM_EXEC 标志了内存区域中页面的读、写和执行权限。这些标志根据要求组合构成 VMA 的访问控制权限,当访问 VMA 时,需要查看其访问权限。比如进程的对象代码映射区域可能会标志为 VM_READ 和 VM_EXEC,而没有标志为 VM_WRITE;另一方面,可执行对象数据段的映射区域标志为 VM_READ 和 VM_WRITE,而 VM_EXEC 标志对它就毫无意义。也就是说,只读文件数据段的映射区域仅可被标志为 VM_READ。
VM_SHARD 指明了内存区域包含的映射是否可以在多进程间共享,如果该标志被设置,则我们称其为共享映射; 如果未被设置,而仅仅只有一个进程可以使用该映射的内容,我们称它为私有映射。
VM_IO 标志内存区域中包含对设备空间的映射。该标志通常在设备驱动程序执行 mmap()
函数进行 I/O 空间映射时才被设置,同时该标志也表示该内存区域不能被包含在任何进程的存放转存(core dump)中。VM_RESERVED 标志规定了内存区域不能被换出,它也是在设备驱动程序进行映射时被设置。
VM_SEQ_READ 标志暗示内核应用程序对映射内容执行有序的(线性和连续的)读操作;这样,内核可以有选择地执行预读文件。VM_RAND_READ 标志的意义正好相反,暗示应用程序对映射内容执行随机的(非有序的)读操作。因此内核可以有选择地减少或彻底取消文件预读,所以这两个标志可以通过系统调用 madvise()
设置,设置参数分别是 MADV_SEQUENTIAL 和 MADV_RANDOM。文件预读是指在读数据时有意地按顺序多读取一些本次请求以外的数据——希望多读的数据能够很快就被用到。这种预读行为对那些顺序读取数据的应用程序有很大的好处,但是如果数据的访问是随机的那么预读显然就多余了。
3.2. VMA 操作
vm_area_struct
结构体中的 vm_ops
域指向与指定内存区域相关的操作函数表,内核使用表中的方法操作 VMA。 vm_area_struct
作为通用对象代表了任何类型的内存区域,而操作表描述针对特定的对象实例的特定方法。
操作函数表由 vm_operations_struct
结构体表示,定义在文件<linux/mm.h>中:
struct vm_operations_struct { void (*open) (struct vm_area_struct *); // 当指定内存区域被加入到一个地址空间时,该函数被调用 void (*close) (struct vm_area_struct *); // 当指定内存区域从地址空间删除时,该函数被调用 int (*fault) (struct vm_area_struct *, struct vm_fault *); // 当没有出现在物理内存中的页面被访问时,该函数被页面故障处理调用 int (*page_mkwrite) (struct vm_area_struct *vma, struct vm_fault *vmf); // 当某个页面为只读页面时,该函数被页面故障处理调用 int (*access) (struct vm_area_struct *, unsigned long , // 当 get_user_pages() 函数调用失败时,该函数被 access_process_vm() 函数调用 void *, int, int); };
3.3. 内存区域的树型结构和内存区域的链表结构
前面介绍过,可以通过内存描述符中的 mmap
(链表)和 mm_rb
(红黑树)域之一访问内存区域。这两个域各自独立地指向与内存描述符相关的全体内存区域对象。其实,它们包含完全相同的 vm_area_struct
结构体的指针,仅仅组织方法不同。
链表用于需要遍历全部节点的时候,而红黑树适用于在地址空间中定位特定内存区域的时候。内核为了内存区域上的各种不同操作都能获得高性能,所以同时使用了这两种数据结构。
3.4. 实际使用中的内存区域
可以使用 /proc/<pid>/maps
文件系统或者 pmap
工具查看给定进程的内存空间和其中所含的内存区域。
下面是一个例子:
rlove@wolf:~$ cat /proc/1426/maps 00e80000-00faf000 r-xp 00000000 03:01 208530 /lib/tls/libc-2.5.1.so 00faf000-00fb2000 rw-p 0012f000 03:01 208530 /lib/tls/libc-2.5.1.so 00fb2000-00fb4000 rw-p 00000000 00:00 0 08048000-08049000 r-xp 00000000 03:03 439029 /home/rlove/src/example 08049000-0804a000 rw-p 00000000 03:03 439029 /home/rlove/src/example 40000000-40015000 r-xp 00000000 03:01 80276 /lib/ld-2.5.1.so 40015000-40016000 rw-p 00015000 03:01 80276 /lib/ld-2.5.1.so 4001e000-4001f000 rw-p 00000000 00:00 0 bfffe000-c0000000 rwxp fffff000 00:00 0
每行数据格式如下:
开始-结束 访问权限 偏移 主设备号:次设备号 inode 文件
注意没有映射文件的内存区域的设备标志为 00:00,inode 标志也为 0,这个区域就是零页——零页映射的内容全为零。
4. 操作内存区域
内核时常需要在某个内存区域上执行一些操作,比如某个指定地址是否包含在某个内存区域中。这类操作非常频繁,另外它们也是 mmap()
例程的基础,后面将介绍它。
为了方便执行这类对内存区域的操作,内核定义了许多的辅助函数。它们都声明在文件<linux/mm.h>中。
4.1. find_vma()
为了找到一个给定的内存地址属于哪一个内存区域,内核提供了 find_vma()
函数。该函数定义在文件<mm/mmap.c>中:
struct vm_area_struct find_vma(struct mm struct *mm, unsigned long addr);
该函数在指定的地址空间中搜索第一个 vm_end 大于 addr 的内存区域。换句话说,该函数寻找第一个包含 addr 或首地址大于 addr 的内存区域,如果没有发现这样的区域,该函数返回 NULL;否则返回指向匹配的内存区域的 vm_area_struct 结构体指针。注意,由于返回的 VMA 首地址可能大于 addr,所以指定的地址并不一定就包含在返回的 VMA 中。因为很有可能在对某个 VMA 执行操作后,还有其他更多的操作会对该 VMA 接着进行操作,所以 find_vma()
函数返回的结果被缓存在内存描述符的 mmap_cache
域中。实践证明,被缓存的 VMA 会有相当好的命中率(实践中大约 30%~40%),而且检查被缓存的 VMA 速度会很快,如果指定的地址不在缓存中,那么必须搜索和内存描述符相关的所有内存区域。这种搜索通过红黑树进行:
struct vm_area_struct * find_vma(struct mm_struct *mm, unsigned long addr) { struct vm_area_struct *vma = NULL; if (mm) { vma = mm->mmap_cache; if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) { struct rb_node *rb_node; rb_node = mm->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->vm_end > addr) { vma = vma_tmp; if (vma_tmp->vm_start <= addr) break; rb_node = rb_node->rb_left; } else rb_node = rb_node->rb_right; } if (vma) mm->mmap_cache = vma; } } return vma; }
首先,该函数检查 mmap_cache
,看看缓存的 VMA 是否包含了所需地址。注意简单地检查 VMA 的 vm_end
是否大于 addr,并不能保证该 VMA 是第一个大于 addr 的内存区域,所以缓存要想发挥作用,就要求指定的地址必须包含在被缓存的 VMA 中——幸好,这也正是连续操作同一 VMA 必然发生的情况。
如果缓存中并未包含希望的 VMA,那么该函数必须搜索红黑树。如果当前 VMA 的 vm_end
大于 addr,进入左子节点继续搜索否则,沿右边子节点搜索,直到找到包含 addr 的 VMA 为止。如果没有包含 addr 的 VMA 被找到,那么该函数继续搜索树,并且返回大于 addr 的第一个 VMA。如果也不存在满足要求的 VMA,那该函数返回 NULL。
4.2. find_vma_prev()
find_vma_prev()
函数和 find_vma()
工作方式相同,但是它返回第一个小于 addr 的 VMA。该函数定义和声明分别在文件 mm/mmap.c 中和文件<linux/mm.h>中:
struct vm_area_struct * find_vma_prev(struct mm_struct *mm, unsigned long addr, struct vm_area_struct **pprev)
pprev
参数存放指向先于 addr 的 VMA 指针。
4.3. find_vma_intersection()
find_vma_intersection()
函数返回第一个和指定地址区间相交的 VMA。因为该函数是内联函数,所以定义在文件<linux/mm.h>中:
static inline struct vm_area_struct * find_vma_intersection(struct mm_struct *mm, unsigned long start_addr, unsigned long end_addr) { struct vm_area_struct *vma; vma = find_vma(mm, start_addr); if (vma && end_addr <= vma->vm_start) vma = NULL; return vma; }
第一个参数 mm
是要搜索的地址空间, star_addr
是区间的开始首位置, end_addr
是区间的尾位置。
显然,如果 find_vma()
返回 NULL,那么 find_vma_interesection()
也会返回 NULL。但是如果 find_vma()
返回有效的 VMA, find_vma_intersection()
只有在该 VMA 的起始位置于给定的地址区间结束位置之前,才将其返回。如果 VMA 的起始位置大于指定地址范围的结束位置,则该函数返回 NULL。
5. mmap() 和 do_mmap():创建地址区间
内核使用 do_mmap()
函数创建一个新的线性地址区间。但是说该函数创建了一个新 VMA 并不非常准确,因为如果创建的地址区间和一个已经存在的地址区间相邻,并且它们具有相同的访问权限的话,两个区间将合并为一个,如果不能合并,就确实需要创建一个新的 VMA 了。但无论哪种情况, do_mmap()
函数都会将一个地址区间加入到进程的地址空间中——无论是扩展已存在的内存区域还是创建一个新的区域。
do_mmap()
函数定义在文件<linux/mm.h>中。
unsigned long do_mmap(struct file *file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flag, unsigned long offset)
该函数映射由 file 指定的文件,具体映射的是文件 offset 中从偏移处开始,长度为 len 字节的范围内的数据。如果 file 参数是 NULL 并且 offset 参数也是 0,那么就代表这次映射没有和文件相关,该情况称作匿名映射(anonymous mapping)。如果指定了文件名和偏移量,那么该映射称为文件映射(file-backed mapping)。addr 是可选参数,它指定搜索空闲区域的起始位置。prot 参数指定内存区域中页面的访问权限。flag 参数指定了 VMA 标志,这些标志指定类型并改变映射的行为。
在用户空间可以通过 mmap()/mmap2()
系统调用获取内核函数 do_mmap()
的功能。
6. munmap() 和 do_munmap():删除地址区间
内核使用 do_munmap()
函数从特定的进程地址空间中删除指定地址区间,该函数定义在文件<linux/mm.h>中。
在用户空间可以通过 munmap()
系统调用获取内核函数 do_munmap()
的功能。
7. 页表(Page Tables)
虽然应用程序操作的对象是映射到物理内存之上的虚拟内存,但是处理器直接操作的却是物理内存。所以当用程序访问一个虚拟地址时,首先必须将虚拟地址转化成物理地址,然后处理器才能解析地址访问请求。 地址的转换工作需要通过“查询页表”才能完成 ,概括地讲,地址转换需要将虚拟地址分段(前面介绍过 Linux 采用是的平坦地址空间而不是分段式的内存模式,也可以说只使用一个段),使每段虚拟地址都作为一个索引指向页表,而页表项则指向下一级别的页表或者指向最终的物理页面。
7.1. 查询页表
Linux 使用四级页表完成地址转换:
- 顶级页表是页全局目录(PGD),它包含了一个 pgd_t 类型数组,多数体系结构中 pgd_t 类型等同于无符号长整型类型。PGD 中的表项指向二级页目录中的表项:PUD。
- 二级页表是页上层目录(PUD),它是个 pud_t 类型数组,其中的表项指向 PMD 中的表项。
- 三级页表是中间页目录(PMD),它是个 pmd_t 类型数组,其中的表项指向 PTE 中的表项。
- 最后一级的页表简称页表,其中包含了 pet_t 类型的页表项,该页表项指向物理页面。
多数体系结构中,搜索页表的工作是由硬件完成的(至少某种程度上)。虽然通常操作中,很多使用页表的工作都可以由硬件执行,但是只有在内核正确设置页表的前提下,硬件才能方便地操作它们。图 1 描述了虚拟地址通过页表找到物理地址的过程。
每个进程都有自己的页表(当然,线程会共享页表)内存描述符的 pgd
域指向的就是进程的页全局目录。注意,操作和检索页表时必须使用 page_table_lock
锁,该锁在相应的进程的内存描述符中,以防止竞争条件。
Figure 1: 虚拟地址通过页表找到物理地址
图 2(摘自:深入理解计算机系统,第 2 版,第 9 章)更新详细地展示了 Core i7/Linux 地址转换过程。
Figure 2: Core i7 page table translation. Legend: PT: page table, PTE: page table entry, VPN: virtual page number, VPO: virtual page offset, PPN: physical page number, PPO: physical page offset.
7.2. TLB
由于几乎每次对虚拟内存中的页面访问都必须先解析它,从而得到物理内存中的对应地址,所以页表操作的性能非常关键。但不幸的是,搜索内存中的物理地址速度很有限,因此为了加快搜索,多数体系结构都实现了一个 TLB(Translation Lookaside Buffer,TLB)。 TLB 作为一个将虚拟地址映射到物理地址的硬件缓存,当请求访问一个虚拟地址时,处理器将首先检查 TLB 中是否缓存了该虚拟地址到物理地址的映射,如果在缓存中直接命中,物理地址立刻返回否则,就需要再通过页表搜索需要的物理地址。
图 3(摘自:深入理解计算机系统,第 2 版,第 9 章)展示了 TLB 的工作原理。
Figure 3: Summary of Core i7 address translation. For simplicity, the i-caches, i-TLB, and L2 unified TLB are not shown.
7.2.1. 查看 TLB miss
使用 perf 可以查看 TLB miss 的情况。如:
$ perf stat -e dTLB-load-misses,iTLB-load-misses /bin/ls > /dev/null Performance counter stats for '/bin/ls': 5,775 dTLB-load-misses 1,059 iTLB-load-misses 0.001897682 seconds time elapsed
参考:
https://unix.stackexchange.com/questions/29853/command-to-measure-tlb-misses-on-linux
https://perf.wiki.kernel.org/index.php/Tutorial#Events
8. 参考
本文主要摘自《Linux 内核设计与实现,第 3 版》