当前位置:网站首页>创建进程内存管理copy_mm - 进程与线程(九)
创建进程内存管理copy_mm - 进程与线程(九)
2022-04-23 05:26:00 【生活需要深度】
对于进程,除了0号进程,其他的所有进程(无论是内核线程还是普通线程)都是通过fork出来的,而创建进程是在内核中完成的
要么在内核空间直接创建出所谓的内核线程
要么是应用空间通过fork/clone/vfork这样的系统调用进入内核,再内核空间创建
同上一章,我们完成的分析了fork的整个过程,fork分为两部分,一部分是初始化进程控制块,另外一部分是进程管理部分。本章的重点学习以下内容
子进程如何构建自己的内存管理
父子进程如何共享地址空间
写时复制如何发生
1. 写时复制技术
在传统的unix操作系统中,创建新建成时就会复制父进程所拥有的所有资源,这样进程的创建就变的很低效。其原因如下
每次创建子进程时,都要把父进程的进程地址空间中的内容复制到子进程,但是子进程甚至不用父进程的资源
子进程调用execve()系统调用之后,可能和父进程分道扬镳
所以现在的操作系统都采用写时复制(COW,Copy on Write)技术进行优化,其原理如下
父进程在创建子进程的时,不需要复制进程地址空间的内容到子进程,只需要复制父进程的进程地址空间的页表到子进程,并将页面属性修改为只读,这样父、子进程就共享相同的物理内存。
当父、子进程中有一方需要修改某个物理页面的内容,触发写保护的缺页异常,然后才复制共享页面的内容,从而让父、子进程拥有各自的副本
也就是说,进程地址空间以只读的方式共享,当需要写入时,才发生复制
在采用写时复制技术的Linux内核中,用fork函数创建一个新进程的开销就变得很小,免去了复制父进程整个进程地址空间导致的巨大开销,现在只需要复制父进程页表就可以了。
2. copy的内存管理
2.1 内存初始化
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
//初始化task的mm_struct和VMA为空
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; //current宏表明当前进程,即父进程
if (!oldmm) //如果父进程使一个没有进程地址空间的内核线程,不需要为子进程做内存复制,直接退出
return 0;
/* initialize the new vmacache entries */
vmacache_flush(tsk);
//如果调用vfork创建子进程,那么CLONE_VM标志位就会被置位,因此子进程进程的mm直接指向父进程的内存描述符
if (clone_flags & CLONE_VM) {
atomic_inc(&oldmm->mm_users);
mm = oldmm;
goto good_mm;
}
//如果CLONE_VM没有置位,那么调用dump_mm来复制父进程的进程地址空间
retval = -ENOMEM;
mm = dup_mm(tsk);
if (!mm)
goto fail_nomem;
good_mm:
tsk->mm = mm;
tsk->active_mm = mm;
return 0;
fail_nomem:
return retval;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
该函数比较简单,其主要做了以下几件事情
如果使内核线程,也就是当前进程地址空间为空,就不需要为子进程做内存复制,直接退出
在创建的时候,会根据fork参数的clone_flags来决定,如果是CLONE_VM标志位就会被置位,子进程的mm指针指向父进程的内存描述符的mm即可
如果CLONE_VM没有被置为,那么调用dump_mm来复制父进程的进程地址空间
dum_mm函数实现也在fork.c文件中,实现也比较简单
static struct mm_struct *dup_mm(struct task_struct *tsk)
{
struct mm_struct *mm, *oldmm = current->mm;
int err;
//1.通过allocate_mm分配属于进程自己的mm_struct结构来管理自己的地址空间
mm = allocate_mm();
if (!mm)
goto fail_nomem;
memcpy(mm, oldmm, sizeof(*mm));
//2.通过mm_init来初始化mm_struct中相关成员
if (!mm_init(mm, tsk, mm->user_ns))
goto fail_nomem;
//3.通过dup_mmap来复制父进程的地址空间
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
分配mm_struct结构就不需要赘述,我们先看下mm_init,其函数如下
static struct mm_struct *mm_init(struct mm_struct *mm, struct task_struct *p,
struct user_namespace *user_ns)
{
// 1. 初始化mm
mm->mmap = NULL;
mm->mm_rb = RB_ROOT;
mm->vmacache_seqnum = 0;
atomic_set(&mm->mm_users, 1);
atomic_set(&mm->mm_count, 1);
init_rwsem(&mm->mmap_sem);
INIT_LIST_HEAD(&mm->mmlist);
mm->core_state = NULL;
atomic_long_set(&mm->nr_ptes, 0);
mm_nr_pmds_init(mm);
mm->map_count = 0;
mm->locked_vm = 0;
mm->pinned_vm = 0;
memset(&mm->rss_stat, 0, sizeof(mm->rss_stat));
spin_lock_init(&mm->page_table_lock);
mm_init_cpumask(mm);
mm_init_aio(mm);
mm_init_owner(mm, p);
RCU_INIT_POINTER(mm->exe_file, NULL);
mmu_notifier_mm_init(mm);
clear_tlb_flush_pending(mm);
#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && !USE_SPLIT_PMD_PTLOCKS
mm->pmd_huge_pte = NULL;
#endif
mm_init_uprobes_state(mm);
if (current->mm) {
mm->flags = current->mm->flags & MMF_INIT_MASK;
mm->def_flags = current->mm->def_flags & VM_INIT_DEF_MASK;
} else {
mm->flags = default_dump_filter;
mm->def_flags = 0;
}
//2,分配一个进程私有的page页,当需要va->pa转换的时候,查找属于当前进程的pgd表项
if (mm_alloc_pgd(mm))
goto fail_nopgd;
//3. 设置了mm->context.id为0,当进程调度的时候进行地址空间切换,如果mm->context.id为0就为进程分配新的ASID
if (init_new_context(p, mm))
goto fail_nocontext;
mm->user_ns = get_user_ns(user_ns);
return mm;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
每个进程在创建的时都会分配一级页表,并且内存描述符中一个pdg的成员指向这个进程的一级页表的基地址。当进程初始化完成后,需要转换,进程切换的时候,会使用tsk->mm->pgd指向的页表作为base来进程页表中遍历,对于ARM64架构来说,他们由两个页表的基地址寄存器ttbr0_el1和ttbr1_el0。
讲完mm_init相关的内容,接着返回dum_mmap,dup_mmap函数参数中,mm表示新进程的mm_struct数据结构,oldmm表示父进程的mm_struct数据结构。该函数的主要作用是遍历父进程中所有的VMA,然后复制父进程VMA中对应的PTE到子进程的VMA对应的PTE中。注意,只是复制PTE,并不是复制VMA对应页面的内容。
static __latent_entropy int dup_mmap(struct mm_struct *mm,
struct mm_struct *oldmm)
{
down_write_killable(&oldmm->mmap_sem);
for (mpnt = oldmm->mmap; mpnt; mpnt = mpnt->vm_next) {
INIT_LIST_HEAD(&tmp->anon_vma_chain);
retval = vma_dup_policy(mpnt, tmp);
if (retval)
goto fail_nomem_policy;
tmp->vm_mm = mm;
if (anon_vma_fork(tmp, mpnt))
goto fail_nomem_anon_vma_fork;
__vma_link_rb(mm, tmp, rb_link, rb_parent);
rb_link = &tmp->vm_rb.rb_right;
rb_parent = &tmp->vm_rb;
mm->map_count++;
retval = copy_page_range(mm, oldmm, mpnt);
}
up_write(&mm->mmap_sem);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
由于后续会修改父进程的进程地址空间,因此要给父进程加上一个写类型的信号量
通过fork循环遍历父进程中所有的VMA,进程中所有的VMA都会添加到内存描述符的mmap成员指向的链表中
vma_dup_policy为子进程创建一个VMA,子进程VMA中有一个链表aon_vma_chain,用于存放aon_vma_chain数据结构,用在RMAP机制中
anon_vma_fork函数创建属于子进程的aon_vma数据结构,并使用aon_vma_chain来实现父子进程VMA的链接
__vma_link_rb把刚才创建的VMA插入子进程的mm
copy_page_range复制父进程VMA的页表到子进程页表中
对于每一个vma都调用copy_page_range,此函数会遍历vma中每一个虚拟页,然后拷贝父进程的页表到子进程(虚拟页对应的页表存在的话),这里主要是页表遍历的代码,从pgd->copy_page_range->copy_pud_range->copy_pmd_range->copy_pte_range
static int copy_pte_range(struct mm_struct *dst_mm, struct mm_struct *src_mm,
pmd_t *dst_pmd, pmd_t *src_pmd, struct vm_area_struct *vma,
unsigned long addr, unsigned long end)
{
do {
/*
* We are holding two locks at this point - either of them
* could generate latencies in another task on another CPU.
*/
if (progress >= 32) {
progress = 0;
if (need_resched() ||
spin_needbreak(src_ptl) || spin_needbreak(dst_ptl))
break;
}
if (pte_none(*src_pte)) {
progress++;
continue;
}
entry.val = copy_one_pte(dst_mm, src_mm, dst_pte, src_pte,
vma, addr, rss);
if (entry.val)
break;
progress += 8;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
我们看的在copy_present_pte函数中,对父子进程的写保护处理,也就是当发现父进程的vma的属性为私有可写的时候,就设置父进程和子进程的相关的页表项为只读。这点很重要,因为这样既保证了父子进程的地址空间的共享(读的时候),又保证了他们有独立的地址空间(写的时候)。
2.2 写时复制发生
ork创建完子进程后,通过复制父进程的页表来共享父进程的地址空间,我们知道对于私有的可写的页,设置了父子进程的相应页表为为只读,这样就为写实复制创造了页表层面上的条件。当父进程或者子进程,写写保护的页时触发访问权限异常:
... //处理器架构处理
do_page_fault // arch/arm64/mm/fault.c
-> __do_page_fault
-> handle_mm_fault
-> handle_pte_fault //mm/memory.c
-> if (vmf->flags & FAULT_FLAG_WRITE) {
if (!pte_write(entry))
return do_wp_page(vmf);
entry = pte_mkdirty(entry);
}
1
2
3
4
5
6
7
8
9
10
这一章的详细内容,请参考linux内存管理笔记(三十六)----写时复制
3. 总结
fork的时候会创建内核管理初始化,例如mm_struct, vma等用于描述进程自己的地址空间,然后会创建出进程私有的pgd页,用于页表遍历时填充页表,然后还会拷贝父进程所有的vma,然后就是对于每个vma做页表的拷贝和写保护操作。后面的pud pmd的其他各级页表的创建和填充工作由缺页异常处理来完成。
版权声明
本文为[生活需要深度]所创,转载请带上原文链接,感谢
https://blog.csdn.net/u012294613/article/details/124351343
边栏推荐
- egg测试的知识大全--mock、superTest、coffee
- d.ts---更详细的知识还是需要看官网的介绍(声明文件章节)
- es6数组的使用
- Mairadb数据库基本操作之数据管理
- 改进DevSecOps框架的 5 大关键技术
- Ehcache Memcache redis three caches
- PIP free export with path (@ file: / / /) notes
- Study notes: unity customsrp-12-hdr
- 看板快速启动指南
- What are the reasons for the failure of digital transformation?
猜你喜欢
Getting started with varnish
Requirements for SQL server to retrieve SQL and user information
How to add beautiful code blocks in word | a very complete method to sort out and compare
Three of three JS (webgl) is simple to draw lines / arcs according to points (based on linegeometry / line2 / linematerial, draw two arc segments based on the center of the circle)
Laravel routing settings
2021-10-25
Box collapse and margin collapse
SQL Server检索SQL和用户信息的需求
Publish your own wheel - pypi packaging upload practice
My old programmer's perception of the dangers and opportunities of the times?
随机推荐
!!!!!!!!!!!!!!!!!!
Modèle axé sur le domaine DDD (III) - gestion des transactions à l'aide de Saga
Why can't V-IF and V-for be used together
How to set the initial value of El input number to null
2021-09-28
引入精益管理方式,需要提前做到这九点
How to realize adaptive layout
varnish入门
領域驅動模型DDD(三)——使用Saga管理事務
MySQL series - install MySQL 5.6.27 on Linux and solve common problems
Let the LAN group use the remote device
What are the reasons for the failure of digital transformation?
Cross domain CORS relationship~
Requirements for SQL server to retrieve SQL and user information
Uniapp wechat sharing
Graphics.FromImage报错“Graphics object cannot be created from an image that has an indexed pixel ...”
MFC实现资源单独Dll实现
使用宝塔+xdebug+vscode远程调试代码
Branch and loop statements
WTL 自绘控件库 (CQsCheckComboxBox)