用户态和内核态的划分
struct mm_struct 结构来管理内存,是struct task_struct中的一个成员变量
\include\linux\sched.h
struct mm_struct *mm;
task_size 是struct mm_struct 里面的一个成员变量
\include\linux\mm_types.h
unsigned long task_size; /* size of task vm space */
整个虚拟内存空间要一分为二,一部分是用户态地址空间,一部分是内核态地址空间,那这两部分的分界线就要是用 task_size 来定义
TASK_SIZE 定义
\arch\x86\include\asm\processor.h
#ifdef CONFIG_X86_32
/*
* User space process size: 3GB (default).
*/
#define TASK_SIZE PAGE_OFFSET
#define TASK_SIZE_MAX TASK_SIZE
/*
config PAGE_OFFSET
hex
default 0xC0000000
depends on X86_32
*/
#else
/*
* User space process size. 47bits minus one guard page.
*/
#define TASK_SIZE_MAX ((1UL << 47) - PAGE_SIZE)
#define TASK_SIZE (test_thread_flag(TIF_ADDR32) ? \
IA32_PAGE_OFFSET : TASK_SIZE_MAX)
......
当执行一个新的进程的时候,会进行这样的设置:
current->mm->task_size = TASK_SIZE;
32 位的系统
- 最大能够寻址 2^32=4G,其中用户态虚拟地址空间是 3G,内核态是 1G。
64 位系统
虚拟地址只使用了 48 位
- 1 左移了 47 位,就相当于 48 位地址空间一半的位置,0x0000800000000000
#define TASK_SIZE_MAX ((1UL << 47) - PAGE_SIZE)
- 然后减去一个页,就是 0x00007FFFFFFFF000,共 128T。
- 内核空间也是 128T。内核空间和用户空间之间隔着很大的空隙,以此来进行隔离。
图片来自极客时间趣谈linux操作系统
用户态布局
用户态虚拟空间里面的几类数据
struct mm_struct 里面
\include\linux\mm_types.h
unsigned long mmap_base; /* base of mmap area */
unsigned long total_vm; /* Total pages mapped */
unsigned long locked_vm; /* Pages that have PG_mlocked set */
unsigned long pinned_vm; /* Refcount permanently increased */
unsigned long data_vm; /* VM_WRITE & ~VM_SHARED & ~VM_STACK */
unsigned long exec_vm; /* VM_EXEC & ~VM_WRITE & ~VM_STACK */
unsigned long stack_vm; /* VM_STACK */
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;
- total_vm
总共映射的页的数目 - locked_vm
就是被锁定不能换出 - pinned_vm
不能换出,也不能移动 - data_vm
存放数据的页的数目 - exec_vm
存放可执行文件的页的数目 - stack_vm
栈所占的页的数目 - start_code
可执行代码的开始位置 - end_code
可执行代码的结束位置 - start_data
已初始化数据的开始位置 - end_data
已初始化数据的结束位置 - start_brk
堆的起始位置 - brk
堆当前的结束位置,malloc 申请一小块内存的话,就是通过改变 brk 位置实现的 - start_stack
栈的起始位置,栈的结束位置在寄存器的栈顶指针中 - arg_start 和 arg_end
参数列表的位置 - env_start 和 env_end
环境变量的位置 - mmap_base
虚拟地址空间中用于内存映射的起始地址
布局图
图片来自极客时间趣谈linux操作系统
虽然 32 位和 64 位的空间相差很大,但是区域的类别和布局是相似的
区域的属性描述
使用 vm_area_struct 结构
\include\linux\mm_types.h
struct vm_area_struct *mmap; /* list of VMAs */
struct rb_root mm_rb;
vm_area_struct 定义
\include\linux\mm_types.h
struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */
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, *vm_prev;
struct rb_node vm_rb;
struct mm_struct *vm_mm; /* The address space we belong to. */
struct list_head anon_vma_chain; /* Serialized by mmap_sem &
* 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;
struct file * vm_file; /* File we map to (can be NULL). */
void * vm_private_data; /* was vm_pte (shared mem) */
} __randomize_layout;
- vm_start 和 vm_end 指定了该区域在用户空间中的起始和结束地址。
- vm_next 和 vm_prev 将这个区域串在链表上
- vm_rb 将这个区域放在红黑树上
- vm_ops 里面是对这个内存区域可以做的操作的定义
- anon_vma 中anoy 是 anonymous,匿名的意思,匿名映射
- vm_file 指定被映射的文件
vm_area_struct 是如何和内存区域几类数据关联
虚拟内存区域可以映射到物理内存,也可以映射到文件,映射到物理内存的时候称为匿名映射,anon_vma 中,anoy 就是 anonymous,匿名的意思,映射到文件就需要有 vm_file 指定被映射的文件。
映射是在load_elf_binary里面实现的,加载内核的是它,启动第一个用户态进程 init 的是它,fork 完了以后,调用 exec 运行一个二进制程序的也是它,exec 运行一个二进制程序的时候,除了解析 ELF 的格式之外,另外一个重要的事情就是建立内存映射。
load_elf_binary 函数
\fs\binfmt_elf.c
static int load_elf_binary(struct linux_binprm *bprm)
{
......
setup_new_exec(bprm);
......
retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
executable_stack);
......
error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
elf_prot, elf_flags, total_size);
......
retval = set_brk(elf_bss, elf_brk, bss_prot);
......
elf_entry = load_elf_interp(&loc->interp_elf_ex,
interpreter,
&interp_map_addr,
load_bias, interp_elf_phdata);
......
current->mm->end_code = end_code;
current->mm->start_code = start_code;
current->mm->start_data = start_data;
current->mm->end_data = end_data;
current->mm->start_stack = bprm->p;
......
}
load_elf_binary 做了以下的事情:
- 调用 setup_new_exec,设置内存映射区 mmap_base;
- 调用 setup_arg_pages,设置栈的 vm_area_struct,这里面设置了 mm->arg_start 是指向栈底的,current->mm->start_stack 就是栈底;
- elf_map 会将 ELF 文件中的代码部分映射到内存中来;
- set_brk 设置了堆的 vm_area_struct,这里面设置了 current->mm->start_brk = current->mm->brk,也即堆里面还是空的;
- load_elf_interp 将依赖的 so 映射到内存中的内存映射区域。
形成下面这个内存映射图
图片来自极客时间趣谈linux操作系统
映射完毕后,什么情况下会修改呢?
- 第一种情况是函数的调用,涉及函数栈的改变,主要是改变栈顶指针。
- 第二种情况是通过 malloc 申请一个堆内的空间,当然底层要么执行 brk,要么执行 mmap。
内核态的布局
内核态的虚拟空间和某一个进程没有关系,所有进程通过系统调用进入到内核之后,看到的虚拟地址空间都是一样的。
在内核态,由于 32 位内核态空间太小了,32 位和 64 位的布局差别比较大
32 位的内核态的布局
图片来自极客时间趣谈linux操作系统
32 位的内核态虚拟地址空间一共就 1G,占绝大部分的前 896M,称为直接映射区
直接映射区
所谓的直接映射区,就是这一块空间是连续的,和物理内存是非常简单的映射关系,其实就是虚拟内存地址减去 3G,就得到物理内存的位置。(虚拟地址 - 3G = 物理地址)
直接映射区也需要建立页表, 通过虚拟地址访问(除了内存管理模块)
直接映射区组成: 1MB 启动时占用; 然后是内核代码/全局变量/BSS等,即 内核 ELF文件内容; 进程task_struct 即内核栈也在其中
896M 这个值在内核中被定义为 high_memory,在此之上常称为“高端内存”
两个宏
__pa(vaddr) 返回与虚拟地址 vaddr 相关的物理地址;
__va(paddr) 则计算出对应于物理地址 paddr 的虚拟地址。
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
#define __pa(x) __phys_addr((unsigned long)(x))
#define __phys_addr(x) __phys_addr_nodebug(x)
#define __phys_addr_nodebug(x) ((x) - PAGE_OFFSET)
\arch\x86\include\asm\page.h
\arch\x86\include\asm\page_32.h
剩余虚拟空间组成
- 8MB 空余
在 896M 到 VMALLOC_START 之间有 8M 的空间 - 内核动态映射空间
VMALLOC_START 到 VMALLOC_END 之间称为内核动态映射空间,动态分配内存, 映射放在内核页表中 - 持久内存映射
PKMAP_BASE 到 FIXADDR_START 的空间称为持久内核映射。储存物理页信息 - 固定内存映射
FIXADDR_START 到 FIXADDR_TOP(0xFFFF F000) 的空间,称为固定映射区域,主要用于满足特殊需求。 - 临时内存映射
在最后一个区域可以通过 kmap_atomic 实现临时内核映射,例如为进程映射文件时使用
64 位的内核布局
图片来自极客时间趣谈linux操作系统
- 8T 空余
从 0xffff800000000000 开始就是内核的部分,不过一开始有 8T 的空档区域 - 64T直接映射区域
从__PAGE_OFFSET_BASE(0xffff880000000000) 开始的 64T 的虚拟地址空间是直接映射区域,减去 PAGE_OFFSET 就是物理地址。大部分情况下还是会通过建立页表的方式进行映射 - 32T动态映射
从 VMALLOC_START(0xffffc90000000000)开始到 VMALLOC_END(0xffffe90000000000)的 - 32T 的空间 - 1T物理页描述结构 struct page
从 VMEMMAP_START(0xffffea0000000000)开始的 1T 空间用于存放物理页面的描述结构 struct page 的 - 512MB存放内核代码段、全局变量、BSS 等
从 __START_KERNEL_map(0xffffffff80000000)开始的 512M。减去 __START_KERNEL_map 就能得到物理内存的地址。
总结
内存结构
- 用户态:
- 代码段、全局变量、BSS
- 函数栈
- 堆
- 内存映射区
- 内核态:
- 内核的代码、全局变量、BSS
- 内核数据结构例如 task_struct
- 内核栈
- 内核中动态分配的内存
32 位的对应关系
图片来自极客时间趣谈linux操作系统
64 位的对应关系
图片来自极客时间趣谈linux操作系统
参考资料:
趣谈Linux操作系统(极客时间)链接:
http://gk.link/a/10iXZ
欢迎大家来一起交流学习