总结时刻
还记得俺们上一节俺们搜集项目组需求的时侯,我们晓得一个进程要运行上去须要以下的显存结构。
用户态:
内核态:
如今这种事是不是早已都有了盼头?
我画了一个图,总结一下进程运行状态在32位下对应关系。
对于64位的对应关系,只是稍有区别,我这儿也画了一个图,便捷你对比理解。
用户态和内核态的界定
进程的虚拟地址空间,当然就是站在项目组的角度来看显存,所以我们就从task_struct出发来看。这儿面有一个structmm_struct结构来管理显存。
struct mm_struct *mm;
在structmm_struct上面,有这样一个成员变量:
unsigned long task_size; /* size of task vm space */
我们之前讲过,整个虚拟显存空间要一分为二,一部份是用户态地址空间,一部份是内核态地址空间,那这两部份的分界线在哪儿呢?这就要task_size来定义。
对于32未来的系统,内核上面是这样定义TASK_SIZE答:
#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位地址空间一半的位置,0x00000,之后乘以一个页,就是0xx0000700007FFFFFFFF000,共128T。同样linux下socket编程,内核空间也是128T。内核空间和用户空间之间隔着很大的缝隙,借此来进行隔离。
更多Linux内核视频教程文档资料免费发放后台私信【内核】自行获取。
内核学习网站:
Linux内核源码/显存调优/文件系统/进程管理/设备驱动/网路合同栈-学习视频教程-腾讯课堂
用户态布局
我们先来看用户态虚拟空间的布局。
之前我们讲了用户态虚拟空间上面有几类数据,比如代码、全局变量、堆、栈、内存映射区等。在structmm_struct上面,有下边这种变量定义了这种区域的统计信息和位置。
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表示虚拟地址空间中用于显存映射的起始地址。通常情况下,这个空间是从高地址到低地址下降的。上面俺们讲malloc申请一大块显存的时侯,就是通过mmap在这儿映射一块区域到化学显存。俺们加载动态链接库so文件,也是在这个区域上面,映射一块区域到so文件。
这下所有用户状态的区域的位置基本上都描述清楚了。整个布局犹如下边这张图这样。其实32位和64位置的空间相差很大,而且区域的类别和布局是相像的。
不仅位置信息之外,structmm_struct上面还专门有一个结构vm_area_struct,来描述那些区域的属性。
struct vm_area_struct *mmap; /* list of VMAs */
struct rb_root mm_rb;
这儿面一个是单数组,用于将这种区域串上去。另外还有一个黑红树。又是这个数据结构,在进程调度的时侯我们用的也是黑红树。它的用处就是查找和更改都很快。这儿用黑红树,就是为了快速查找一个显存区域,并在须要改变的时侯,才能快速更改。
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是怎样和前面的显存区域关联的呢?
这个事情是在load_elf_binary上面实现的。没错,就是它。加载内核的是它,启动第一个用户态进程init的是它,fork完了之后,调用exec运行一个二补码程序的也是它。
当exec运行一个二补码程序的时侯,不仅解析ELF的格式之外,另外一个重要的事情就是构建显存映射。
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会完成以下的事情:
最终就产生下边这个显存映射图。
映射完毕后,哪些情况下会更改呢?
第一种情况是函数的调用,涉及函数栈的改变,主要是改变栈顶表针。
第二种情况是通过malloc申请一个堆内的空间,其实底层要么执行brk,要么执行mmap。关于显存映射的部份,我们前面的章节讲,这儿我们重点看一下brk是如何做的。
brk系统调用实现的入口是sys_brk函数,如同下边代码定义的一样。
SYSCALL_DEFINE1(brk, unsigned long, brk)
{
unsigned long retval;
unsigned long newbrk, oldbrk;
struct mm_struct *mm = current->mm;
struct vm_area_struct *next;
......
newbrk = PAGE_ALIGN(brk);
oldbrk = PAGE_ALIGN(mm->brk);
if (oldbrk == newbrk)
goto set_brk;
/* Always allow shrinking brk. */
if (brk brk) {
if (!do_munmap(mm, newbrk, oldbrk-newbrk, &uf))
goto set_brk;
goto out;
}
/* Check against existing mmap mappings. */
next = find_vma(mm, oldbrk);
if (next && newbrk + PAGE_SIZE > vm_start_gap(next))
goto out;
/* Ok, looks good - let it rip. */
if (do_brk(oldbrk, newbrk-oldbrk, &uf) brk = brk;
......
return brk;
out:
retval = mm->brk;
return retval
后面我们讲过了,堆是从低地址向高地址下降的,sys_brk函数的参数brk是新的堆顶位置,而当前的mm->brk是原先堆顶的位置。
首先要做的第一个事情,将原先的堆顶和现今的堆顶,都根据页对齐地址,之后比较大小。假如三者相同,说明此次降低的堆的量很小,还在一个页上面,不须要另行分配页,直接跳到set_brk那儿,设置mm->brk为新的brk就可以了。
假如发觉新旧堆顶不在一个页上面,麻烦了,这下要跨页了。假如发觉新堆顶大于旧堆顶,这说明不是新分配显存了,而是释放显存了,释放的还不小,起码释放了一页,于是调用do_munmap将这一页的显存映射去除。
假如堆即将扩大,就要调用find_vma。假如打开这个函数,看见的是对黑红树的查找,找到的是原堆顶所在的vm_area_struct的下一个vm_area_struct,看当前的堆顶和下一个vm_area_struct之间能够不能分配一个完整的页。假如不能,没办法只得直接退出返回,显存空间都被占满了。
假如还有空间,就调用do_brk进一步分配堆空间,从旧堆顶开始,分配估算出的新旧堆顶之间的页数。
static int do_brk(unsigned long addr, unsigned long len, struct list_head *uf)
{
return do_brk_flags(addr, len, 0, uf);
}
static int do_brk_flags(unsigned long addr, unsigned long request, unsigned long flags, struct list_head *uf)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma, *prev;
unsigned long len;
struct rb_node **rb_link, *rb_parent;
pgoff_t pgoff = addr >> PAGE_SHIFT;
int error;
len = PAGE_ALIGN(request);
......
find_vma_links(mm, addr, addr + len, &prev, &rb_link,
&rb_parent);
......
vma = vma_merge(mm, prev, addr, addr + len, flags,
NULL, NULL, pgoff, NULL, NULL_VM_UFFD_CTX);
if (vma)
goto out;
......
vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
INIT_LIST_HEAD(&vma->anon_vma_chain);
vma->vm_mm = mm;
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_pgoff = pgoff;
vma->vm_flags = flags;
vma->vm_page_prot = vm_get_page_prot(flags);
vma_link(mm, vma, prev, rb_link, rb_parent);
out:
perf_event_mmap(vma);
mm->total_vm += len >> PAGE_SHIFT;
mm->data_vm += len >> PAGE_SHIFT;
if (flags & VM_LOCKED)
mm->locked_vm += (len >> PAGE_SHIFT);
vma->vm_flags |= VM_SOFTDIRTY;
return 0;
在do_brk中,调用find_vma_links找到将来的vm_area_struct节点在黑红树的位置,找到它的父节点、前序节点。接出来调用vma_merge,看这个新节点是否还能和现有树中的节点合并。假如地址是连着的,才能合并,则不用创建新的vm_area_struct了,直接跳到out,更新统计值即可;假如不能合并,则创建新的vm_area_struct,既加到anon_vma_chain数组中,也加到黑红树中。
内核态的布局
用户态虚拟空间剖析完毕,接出来我们剖析内核态虚拟空间。
内核态的虚拟空间和某一个进程没有关系,所有进程通过系统调用步入到内核以后,见到的虚拟地址空间都是一样的。
这儿指出一下,千万别以为到了内核上面,俺们都会直接使用化学显存地址了,轻率地觉得下边讨论的都是化学显存地址,不是的,这儿讨论的还是虚拟显存地址,并且因为内核总是涉及管理化学显存,因此总是隐约约约发生关系,所以这儿必须思路清晰,分清楚化学显存地址和虚拟内存地址。
在内核态,32位和64位的布局差异比较大,主要是由于32位内核态空间太小了。
我们来看32位的内核态的布局。
32位的内核态虚拟地址空间一共就1G,占绝大部份的前896M,我们称为直接映射区。
所谓的直接映射区,就是这一块空间是连续的,和化学显存是十分简单的映射关系,虽然就是虚拟显存地址除以3G,就得到化学显存的位置。
在内核上面,有两个宏:
#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)
然而你要注意,这儿虚拟地址和化学地址发生了关联关系,在化学显存的开始的896M的空间,会被直接映射到3G至3G+896M的虚拟地址,这样容易给你一种觉得,是那些显存访问上去和化学显存差不多,别这样想,在大部份情况下,对于这一段显存的访问,在内核中,还是会使用虚拟地址的,但是将来也会为这一段空间建设页表,对这段地址的访问也会走上一节我们讲的分页地址的流程,只不过页表上面比较简单,是直接的一一对应而已。
这896M还须要仔细分解。在系统启动的时侯,化学显存的前1M早已被占用了,从1M开始加载内核代码段,之后就是内核的全局变量、BSS等,也是ELF上面囊括的。这样内核的代码段,全局变量,BSS也都会被映射到3G后的虚拟地址空间上面。具体的数学显存布局可以查看/proc/iomem。
在内核运行的过程中,假如遇到系统调用创建进程linux内核空间访问用户空间,会创建task_struct这样的实例,内核的进程管理代码会将实例创建在3G至3G+896M的虚拟空间中,其实也会被置于化学显存上面的前896M上面,相应的页表也会被创建。
在内核运行的过程中,会涉及内核栈的分配,内核的进程管理的代码会将内核栈创建在3G至3G+896M的虚拟空间中,其实也都会被置于化学显存上面的前896M上面,相应的页表也会被创建。
896M这个值在内核中被定义为high_memory,在此之上常称为“高端显存”。这是个很宽泛的说法,究竟是虚拟显存的3G+896M以上的是高档显存,还是化学显存896M以上的是高档显存呢?
这儿仍旧须要辨析一下,高档显存是化学显存的概念。它仅仅是内核中的显存管理模块看待化学显存的时侯的概念。上面我们也说过,在内核中,不仅显存管理模块直接操作数学地址之外,内核的其他模块,一直要操作虚拟地址,而虚拟地址是须要显存管理模块分配和映射好的。
假定俺们的笔记本有2G显存,如今若果内核的其他模块想要访问数学显存1.5G的地方,应当如何办呢?假如你认为,我有32位的总线,访问个2G还不小菜一碟,这就错了。
首先,你不能使用数学地址。你须要使用显存管理模块给你分配的虚拟地址,并且虚拟地址的0到3G早已被用户态进程占用去了,你作为内核不能使用。由于你写1.5G的虚拟显存位置,一方面你不晓得应当按照那个进程的页表进行映射;另一方面,即使映射了也不是你真正想访问的化学显存的地方,所以你发觉你作为内核,才能使用的虚拟显存地址,只剩下1G除以896M的空间了。
于是,我们可以将剩下的虚拟显存地址分成下边这几个部份。
32位的内核态布局我们看完了,接出来我们再来看64位的内核布局。
虽然64位的内核布局反倒简单,由于虚拟空间实在是太大了,根本不须要所谓的高档显存,由于内核是128Tlinux内核空间访问用户空间,根本不可能有化学显存超过这个值。
64位的显存布局如图所示。
64位的内核主要包含以下几个部份。
从0xffff8开始就是内核的部份,只不过一开始有8T的空挡区域。
从__PAGE_OFFSET_BASE(0xffffxffff880000000000)开始的64T的虚拟地址空间是直接映射区域linux文件系统,也就是乘以PAGE_OFFSET就是化学地址。虚拟地址和化学地址之间的映射在大部份情况下还是会通过构建页表的形式进行映射。
从VMALLOC_START(0xffffcxffffc90000000000)开始到VMALLOC_END(0xffffexffffe90000000000)的32T的空间是给vmalloc的。
从VMEMMAP_START(0xffffeaxffffea0000000000)开始的1T空间用于储存化学页面的描述结构structpage的。
从__START_KERNEL_map(0xffffffffxffffffff80000000)开始的512M用于储存内核代码段、全局变量、BSS等。这儿对应到化学显存开始的位置,除以__START_KERNEL_map才能得到化学显存的地址。这儿和直接映射区有点像,而且不矛盾,由于直接映射区之前有8T的空当区域,早就过了内核代码在化学显存中加载的位置。
到这儿内核中虚拟空间的布局就介绍完了。