I need a Page!
程序表头涉及到“内存上页的分配”。
进程是个动态的家伙,需要足够的舞台来让他表演,时常嚷嚷着缺空间。
-- kernel/fork.c --
if ((retval = copy_mm(clone_flags, p)))
goto bad_fork_cleanup_signal;
创建子进程时对于mm处理:共享还是分配一个新的?
static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
{
struct mm_struct * mm, *oldmm;
int retval;
tsk->min_flt = tsk->maj_flt = 0;
tsk->nvcsw = tsk->nivcsw = 0;
#ifdef CONFIG_DETECT_HUNG_TASK
tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw;
#endif
tsk->mm = NULL;
tsk->active_mm = NULL;
/*
* Are we cloning a kernel thread?
*
* We need to steal a active VM for that..
*/
oldmm = current->mm;
if (!oldmm)
return 0;
if (clone_flags & CLONE_VM) { //如果内存空间共享,简单了……增加内存描述符mm的引用计数后直接退出
atomic_inc(&oldmm->mm_users);
mm = oldmm;
goto good_mm;
}
retval = -ENOMEM;
mm = dup_mm(tsk); //如果内存空间不共享,则创建一个新的内存空间-->
if (!mm)
goto fail_nomem;
good_mm:
/* Initializing for Swap token stuff */
mm->token_priority = 0;
mm->last_interval = 0;
tsk->mm = mm;
tsk->active_mm = mm;
return 0;
fail_nomem:
return retval;
}
内存空间的分配在代码的表现形式无非是填充 mm_struct 的过程,因为基因的遗传性,有些是可以继承的,有些也会变异。
/*
* Allocate a new mm structure and copy contents from the
* mm structure of the passed in task structure.
*/
struct mm_struct *dup_mm(struct task_struct *tsk)
{
struct mm_struct *mm, *oldmm = current->mm;
int err;
if (!oldmm)
return NULL;
mm = allocate_mm(); // (1)分配一个新的内存描述符mm:kmem_cache_alloc(mm_cachep, GFP_KERNEL)
if (!mm)
goto fail_nomem;
memcpy(mm, oldmm, sizeof(*mm)); // (2)拷贝父进程内存描述符的所有内容
/* Initializing for Swap token stuff */
mm->token_priority = 0;
mm->last_interval = 0;
if (!mm_init(mm, tsk)) // (3)填充新的mm_struct
goto fail_nomem;
/**************************************
mm_init:
if (likely(!mm_alloc_pgd(mm))) { //-->
mm->def_flags = 0;
mmu_notifier_mm_init(mm);
return mm;
}
-->
页全局目录的分配
static inline int mm_alloc_pgd(struct mm_struct * mm)
{
mm->pgd = pgd_alloc(mm);
if (unlikely(!mm->pgd))
return -ENOMEM;
return 0;
}
**************************************/
if (init_new_context(tsk, mm))
goto fail_nocontext;
dup_mm_exe_file(oldmm, mm);
err = dup_mmap(mm, oldmm); //线性区和页表处理 -->
if (err)
goto free_pt;
mm->hiwater_rss = get_mm_rss(mm);
mm->hiwater_vm = mm->total_vm;
if (mm->binfmt && !try_module_get(mm->binfmt->module))
goto free_pt;
return mm;
free_pt:
/* don't put binfmt in mmput, we haven't got module yet */
mm->binfmt = NULL;
mmput(mm);
fail_nomem:
return NULL;
fail_nocontext:
/*
* If init_new_context() failed, we cannot use mmput() to free the mm
* because it calls destroy_context()
*/
mm_free_pgd(mm);
free_mm(mm);
return NULL;
}
翅膀硬了,子进程成了全新的进程,要拥有自己的资源,包括线性区和页表。
“线性区-->线性地址-->虚存映射-->MMU”。
#ifdef CONFIG_MMU
static int dup_mmap(struct mm_struct *mm, struct mm_struct *oldmm)
{
struct vm_area_struct *mpnt, *tmp, *prev, **pprev;
struct rb_node **rb_link, *rb_parent;
int retval;
unsigned long charge;
struct mempolicy *pol;
down_write(&oldmm->mmap_sem);
flush_cache_dup_mm(oldmm);
/*
* Not linked in yet - no deadlock potential:
*/
down_write_nested(&mm->mmap_sem, SINGLE_DEPTH_NESTING);
/*初始化 mm描述符*/
mm->locked_vm = 0;
mm->mmap = NULL;
mm->mmap_cache = NULL;
mm->free_area_cache = oldmm->mmap_base;
mm->cached_hole_size = ~0UL;
mm->map_count = 0;
cpumask_clear(mm_cpumask(mm));
mm->mm_rb = RB_ROOT;
rb_link = &mm->mm_rb.rb_node;
rb_parent = NULL;
pprev = &mm->mmap;
retval = ksm_fork(mm, oldmm);
if (retval)
goto out;
retval = khugepaged_fork(mm, oldmm);
if (retval)
goto out;
prev = NULL;
for (mpnt = oldmm->mmap; mpnt; mpnt = mpnt->vm_next) { // 复制vma线性区
struct file *file;
if(mpnt->vm_flags & VM_DONTCOPY) { //线性区间不允许拷贝,vma减去其大小
longpages = vma_pages(mpnt);
mm->total_vm -= pages;
vm_stat_account(mm, mpnt->vm_flags, mpnt->vm_file,
-pages);
continue;
}
charge = 0;
if (mpnt->vm_flags & VM_ACCOUNT) { //IPC共享线性区,检查是否有足够的内存用于映射
unsigned int len = (mpnt->vm_end - mpnt->vm_start) >> PAGE_SHIFT;
if (security_vm_enough_memory(len))
goto fail_nomem;
charge = len;
}
tmp = kmem_cache_alloc(vm_area_cachep, GFP_KERNEL);
if (!tmp)
goto fail_nomem;
*tmp = *mpnt;
INIT_LIST_HEAD(&tmp->anon_vma_chain);
pol = mpol_dup(vma_policy(mpnt));
retval = PTR_ERR(pol);
if (IS_ERR(pol))
goto fail_nomem_policy;
vma_set_policy(tmp, pol);
tmp->vm_mm = mm;
if (anon_vma_fork(tmp, mpnt))
goto fail_nomem_anon_vma_fork;
tmp->vm_flags &= ~VM_LOCKED;
tmp->vm_next = tmp->vm_prev = NULL;
file = tmp->vm_file;
if (file) { //是否是文件映射
struct inode *inode = file->f_path.dentry->d_inode;
struct address_space *mapping = file->f_mapping;
get_file(file);
if (tmp->vm_flags & VM_DENYWRITE)
atomic_dec(&inode->i_writecount);
mutex_lock(&mapping->i_mmap_mutex);
if (tmp->vm_flags & VM_SHARED)
mapping->i_mmap_writable++;
flush_dcache_mmap_lock(mapping);
/* insert tmp into the share list, just after mpnt */
vma_prio_tree_add(tmp, mpnt);
flush_dcache_mmap_unlock(mapping);
mutex_unlock(&mapping->i_mmap_mutex);
}
/*
* Clear hugetlb-related page reserves for children. This only
* affects MAP_PRIVATE mappings. Faults generated by the child
* are not guaranteed to succeed, even if read-only
*/
if (is_vm_hugetlb_page(tmp))
reset_vma_resv_huge_pages(tmp);
/*
* Link in the new vma and copy the page table entries.
*/
*pprev = tmp;
pprev = &tmp->vm_next;
tmp->vm_prev = prev;
prev = tmp;
__vma_link_rb(mm, tmp, rb_link, rb_parent); //把新分配的vma线性区插入red-black tree
rb_link = &tmp->vm_rb.rb_right;
rb_parent = &tmp->vm_rb;
mm->map_count++;
retval = copy_page_range(mm, oldmm, mpnt); //拷贝页表项
if (tmp->vm_ops && tmp->vm_ops->open)
tmp->vm_ops->open(tmp);
if (retval)
goto out;
}
/* a new mm has just been created */
arch_dup_mmap(oldmm, mm);
retval = 0;
out:
up_write(&mm->mmap_sem);
flush_tlb_mm(oldmm);
up_write(&oldmm->mmap_sem);
return retval;
fail_nomem_anon_vma_fork:
mpol_put(pol);
fail_nomem_policy:
kmem_cache_free(vm_area_cachep, tmp);
fail_nomem:
retval = -ENOMEM;
vm_unacct_memory(charge);
goto out;
}
增加了新的空间,就要映射新的线性地址,也就是虚存;为什么要有虚存,如何实现虚存,这是个问题。
MMU介绍
任务被编译、链接、运行在主存中有重叠的地址,却仍然可以运行。如此一来,对于程序员、编译器便无须考虑给程序分配地址的问题。大家都有自己从零开始的一段虚拟空间。对于上层的透明化,大大的简化了各位的脑细胞死亡率。
“能量是守恒的,世界只是能量的各种表现形式的集合,所以万事万物都该有他守恒的一面”。地址透明化确实让脑细胞的能耗环保了不少,根据守恒定律,这种消耗便转移到了MMU。
那剩下的问题便是MMU如何实现的这种“透明”,这涉及到MMU以及权限等配置,提及MMU就不可避免的牵扯进ARM核中的一个神秘而又无厘头的寄存器CP15。
地址映射本是个频繁的操作,也就是自然的放在了cache。有关的高缓的问题必然是复杂的:直写、回写,流水线造成的的影响,异常的影响……在此省略千字。
MMU对虚存的支持可以使系统具有多个虚拟存储器映射和单个物理存储器映射。
段地址分布
先来个实验,一个可执行程序的段地址分布:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 00008134 000134 000013 00 A 0 0 1 //00008000,arm编译器安排的正文段开始逻辑地址
[ 2] .note.ABI-tag NOTE 00008148 000148 000020 00 A 0 0 4
[ 3] .hash HASH 00008168 000168 000028 04 A 4 0 4
[ 4] .dynsym DYNSYM 00008190 000190 000050 10 A 5 1 4
[ 5] .dynstr STRTAB 000081e0 0001e0 000044 00 A 0 0 1
[ 6] .gnu.version VERSYM 00008224 000224 00000a 02 A 4 0 2
[ 7] .gnu.version_r VERNEED 00008230 000230 000020 00 A 5 1 4
[ 8] .rel.dyn REL 00008250 000250 000008 08 A 4 0 4
[ 9] .rel.plt REL 00008258 000258 000020 08 A 4 11 4
[10] .init PROGBITS 00008278 000278 000010 00 AX 0 0 4 //init
[11] .plt PROGBITS 00008288 000288 000044 04 AX 0 0 4
[12] .text PROGBITS 000082cc 0002cc 000248 00 AX 0 0 4 //_start
[13] .fini PROGBITS 00008514 000514 00000c 00 AX 0 0 4
[14] .rodata PROGBITS 00008520 000520 000004 04 AM 0 0 4
[15] .ARM.exidx ARM_EXIDX 00008524 000524 000008 00 AL 12 0 4
[16] .eh_frame PROGBITS 0000852c 00052c 000004 00 A 0 0 4
[17] .init_array INIT_ARRAY 00010530 000530 000004 00 WA 0 0 4
[18] .fini_array FINI_ARRAY 00010534 000534 000004 00 WA 0 0 4
[19] .jcr PROGBITS 00010538 000538 000004 00 WA 0 0 4
[20] .dynamic DYNAMIC 0001053c 00053c 0000e8 08 WA 5 0 4
[21] .got PROGBITS 00010624 000624 000020 04 WA 0 0 4
[22] .data PROGBITS 00010644 000644 000010 00 WA 0 0 4 //data
[23] .bss NOBITS 00010654 000654 000208 00 WA 0 0 4 //bss
[24] .comment PROGBITS 00000000 000654 000054 00 0 0 1
[25] .debug_aranges PROGBITS 00000000 0006a8 000040 00 0 0 1
[26] .debug_pubnames PROGBITS 00000000 0006e8 000069 00 0 0 1
[27] .debug_info PROGBITS 00000000 000751 000173 00 0 0 1
[28] .debug_abbrev PROGBITS 00000000 0008c4 000116 00 0 0 1
[29] .debug_line PROGBITS 00000000 0009da 00007a 00 0 0 1
[30] .debug_frame PROGBITS 00000000 000a54 0000c8 00 0 0 4
[31] .debug_str PROGBITS 00000000 000b1c 0000b2 01 MS 0 0 1
[32] .debug_loc PROGBITS 00000000 000bce 0000ac 00 0 0 1
[33] .ARM.attributes ARM_ATTRIBUTES 00000000 000c7a 000028 00 0 0 1
[34] .shstrtab STRTAB 00000000 000ca2 00015b 00 0 0 1
[35] .symtab SYMTAB 00000000 0013c8 000750 10 36 86 4
[36] .strtab STRTAB 00000000 001b18 000273 00 0 0 1
注意查看Addr列,换个a.out试试,依然是类似的地址分配,看来虚拟地址是可以重复的,或者你可以认为那只是相对地址。毕竟不同的任务不可能内存区域重复,不同的任务地址设置不同的物理基地址,再加上那个相对地址,便得到最终的物理地址。
可见,物理地址是有 物理基地址和相对地址 共同决定的。
现在有两个任务,都执行到了各自相对地址为08cc的位置,此时发生了任务切换。
按照过去的方式,直接改变为将要运行代码的物理地址。
现在的模式是,通过改变物理基地址使物理地址发生变化。
也就是一个任务对应各自的物理基地址。而物理基地址这样的信息都存储在页表当中。
每个任务,每个进程有自己的一套页表。
内存页表
页表是个什么东西?这个要自己问教科书。具体到arm处理器,有它自己的特点。
ARM MMU有两级页表结构。L1有4096个页表项,每个代表4G/4096 = 1M。
二级页表稍微复杂些,可为粗页,也可以是细页,还可能是微页(之后废弃掉了)。
最后,再次强调下用页表实现虚存的意义。任何一条地址命令在经过页表的转化下都会变成内存上的任意一个地址,对应任意一块空间。关键是,对于外设却是透明的。
无形中,页表的内容成了内存的枢纽,只有页标管理者知道内存哪里放着什么。扩展一个小应用:flash的读写算法,好让读写有寿命限制的flash在空间上能够操作更平均些。
《设计个简单的存储系统》
1.首先,有些必要的区域要确定,低地址向高依次是:
-
- 内核区域,放异常处理跳转。为啥说是个跳转,因为它使用汇编bl,FIQ、IRQ、SWI、UND、ABT异常放在程序的0地址,也就是bin文件的最前头。比如来了个fiq,pc跳转到前途的跳转代码,然后bl到该异常的处理函数。
- 共享区域,放一些大家都用的东西,之所以是大家,因为要设计的是有三个分时运行的任务。进程间共享?IPC? 呵呵,有点这么个意思。
- 页表区域,放页表的地方。一个L1是16K,四个L2(一个系统,三个任务)是4*1K。
- 任务区域,三个任务的虚拟地址一样。
2.如何设计多任务运行。或者一个任务对应自己的一套页表,或者也其他方式,如果任务比较小,比如都小于1M,那么改变L1貌似没有太大的意义,那就改变L1的页表项即可。然后呢,设计的调度器或者调度三套页表的活动与睡眠,或者调度L1的页表项的内容分配为三个任务L2页表的地址。
3.定义各种数据结构,实现。
4.基本的准备有了,开始初始化MMU,cache,写缓冲器。
5.建立上下文切换程序。也就是涉及调度器代码及相关的任务栈等。
阶段性小总结
进程要有线性地址,线性地址是个拥有4G空间的虚拟地址,每个任务拥有各自独立的线性地址体系。线性地址通过页表映射到物理地址,通过控制页表实现任务调度。
readelf -a a.out后看似各段的位置都挨着,但也可能在内存中各据一方,分布内存各地。没有页表,直接读物理地址上的数据几乎是没有意义。
页表的“分配”和“回收”
让我们看下linux内核的页表实现和操作。
“实现”无非就是数据结构的表示;
“操作”就是页表的分配和回收。
当然,说内存难免涉及到高缓,再次表示回避。
分配一个页表:
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0) //-->
-->
static inline struct page *
alloc_pages(gfp_t gfp_mask, unsigned int order)
{
return alloc_pages_current(gfp_mask, order); //-->
}
为当前进程分配页框:
/* Allocate a page from the kernel page pool */
struct page *alloc_pages_current(gfp_t gfp, unsigned order)
{
struct mempolicy *pol = current->mempolicy;
struct page *page;
if (!pol || in_interrupt() || (gfp & __GFP_THISNODE))
pol = &default_policy;
get_mems_allowed();
/* Allocate a page in interleaved policy. */
if (pol->mode == MPOL_INTERLEAVE)
page = alloc_page_interleave(gfp, order, interleave_nodes(pol));
else /* 伙伴系统分配 */
page = __alloc_pages_nodemask(gfp, order,
policy_zonelist(gfp, pol, numa_node_id()),
policy_nodemask(gfp, pol));
put_mems_allowed();
return page;
}
内存管理复杂,因为内存并不像我们想象的那样,给什么来什么,要什么拿什么。内存可是稀缺资源,每一块的分配都牵扯着效率两个字。
首先,内存并不是均匀的,不同物理地址的访问就是有快慢之分;
其次,用户对内存的需求也不是一致的,有时几个页,有时几字节;
再者,地址又有高低端之分,存储也不一定连续等等。
虽繁琐,但大体上可分为 连续页框分配和非连续页框分配;页为基本单位的分配和小于页的内存空间分配。
一个很能说明问题的图,也是最常见的连续页框分配模型。
要说明一些问题,就要抽象出其中关键的部分。进程缺页了,重要的是该分配哪几个页,又该如何给了饥饿的进程。
伙伴系统
隆重推出经典内存分配算法:伙伴系统。
内存空间划分为几个节点,每个节点又划分了几个区,每个区的页有各自的伙伴系统管理。
/*
* This is the 'heart' of the zoned buddy allocator.
*/
struct page *
__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
struct zonelist *zonelist, nodemask_t *nodemask)
{
enum zone_type high_zoneidx = gfp_zone(gfp_mask);
struct zone *preferred_zone;
struct page *page;
int migratetype = allocflags_to_migratetype(gfp_mask);
gfp_mask &= gfp_allowed_mask;
lockdep_trace_alloc(gfp_mask);
might_sleep_if(gfp_mask & __GFP_WAIT);
if (should_fail_alloc_page(gfp_mask, order))
return NULL;
/*
* Check the zones suitable for the gfp_mask contain at least one
* valid zone. It's possible to have an empty zonelist as a result
* of GFP_THISNODE and a memoryless node
*/
if (unlikely(!zonelist->_zonerefs->zone))
return NULL;
get_mems_allowed();
/* The preferred zone is used for statistics later */
first_zones_zonelist(zonelist, high_zoneidx,
nodemask ? : &cpuset_current_mems_allowed, //返回合适的zone
&preferred_zone);
if (!preferred_zone) {
put_mems_allowed();
return NULL;
}
/* First allocation attempt */
page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, nodemask, order, //zone的freelist上找到合适的page
zonelist, high_zoneidx, ALLOC_WMARK_LOW|ALLOC_CPUSET,
preferred_zone, migratetype);
if (unlikely(!page))
page = __alloc_pages_slowpath(gfp_mask, order,
zonelist, high_zoneidx, nodemask,
preferred_zone, migratetype);
put_mems_allowed();
trace_mm_page_alloc(page, order, gfp_mask, migratetype);
return page;
}
伙伴系统的freelist链表挂载着空闲页框,挑几个出来就ok.
伙伴系统可参看:http://blog.chinaunix.net/space.php?uid=20543183&do=blog&id=1930771
就这样,线程有了新的页框。