Linux进程地址管理之mm_struct
FROM : http://www.cnblogs.com/Rofael/archive/2013/04/13/3019153.html
Linux对于内存的管理涉及到非常多的方面,这篇文章首先从对进程虚拟地址空间的管理说起。(所依据的代码是2.6.32.60)
无论是内核线程还是用户进程,对于内核来说,无非都是task_struct这个数据结构的一个实例而已,task_struct被称为进程描述符(process descriptor),因为它记录了这个进程所有的context。其中有一个被称为'内存描述符‘(memory descriptor)的数据结构mm_struct,抽象并描述了Linux视角下管理进程地址空间的所有信息。
mm_struct定义在include/linux/mm_types.h中,其中的域抽象了进程的地址空间,如下图所示:
每个进程都有自己独立的mm_struct,使得每个进程都有一个抽象的平坦的独立的32或64位地址空间,各个进程都在各自的地址空间中相同的地址内存存放不同的数据而且互不干扰。如果进程之间共享相同的地址空间,则被称为线程。
其中[start_code,end_code)表示代码段的地址空间范围。
[start_data,end_start)表示数据段的地址空间范围。
[start_brk,brk)分别表示heap段的起始空间和当前的heap指针。
[start_stack,end_stack)表示stack段的地址空间范围。
mmap_base表示memory mapping段的起始地址。那为什么mmap段没有结束的地址呢?
bbs段是用来干什么的呢?bbs表示的所有没有初始化的全局变量,这样只需要将它们匿名映射为‘零页’,而不用在程序load过程中从磁盘文件显示的mapping,这样既减少了elf二进制文件的大小,也提高了程序加载的效率。在mm_struct中为什么没有bbs段的地址空间表示呢?
除此之外,mm_struct还定义了几个重要的域:
215 atomic_t mm_users; /* How many users with user space? */
216 atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */
这两个counter乍看好像差不多,那Linux使用中有什么区别呢?看代码就是最好的解释了。
681static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
682{
struct mm_struct * mm, *oldmm;
int retval;
tsk->mm = NULL;
tsk->active_mm = NULL;
695 /*
* Are we cloning a kernel thread?
*
* We need to steal a active VM for that..
*/
oldmm = current->mm;
if (!oldmm)
return 0;
704 if (clone_flags & CLONE_VM) {
atomic_inc(&oldmm->mm_users);
mm = oldmm;
goto good_mm;
}
无论我们在调用fork,vfork,clone的时候最终会调用do_fork函数,区别在于vfork和clone会给copy_mm传入一个CLONE_VM的flag,这个标识表示父子进程都运行在同样一个‘虚拟地址空间’上面(在Linux称之为lightweight process或者线程),当然也就共享同样的物理地址空间(Page Frames)。
copy_mm函数中,如果创建线程中有CLONE_VM标识,则表示父子进程共享地址空间和同一个内存描述符,并且只需要将mm_users值+1,也就是说mm_users表示正在引用该地址空间的thread数目,是一个thread level的counter。
mm_count呢?mm_count的理解有点复杂。
对Linux来说,用户进程和内核线程(kernel thread)都是task_struct的实例,唯一的区别是kernel thread是没有进程地址空间的,内核线程也没有mm描述符的,所以内核线程的tsk->mm域是空(NULL)。内核scheduler在进程context switching的时候,会根据tsk->mm判断即将调度的进程是用户进程还是内核线程。但是虽然thread thread不用访问用户进程地址空间,但是仍然需要page table来访问kernel自己的空间。但是幸运的是,对于任何用户进程来说,他们的内核空间都是100%相同的,所以内核可以’borrow'上一个被调用的用户进程的mm中的页表来访问内核地址,这个mm就记录在active_mm。
简而言之就是,对于kernel thread,tsk->mm == NULL表示自己内核线程的身份,而tsk->active_mm是借用上一个用户进程的mm,用mm的page table来访问内核空间。对于用户进程,tsk->mm == tsk->active_mm。
为了支持这个特别,mm_struct里面引入了另外一个counter,mm_count。刚才说过mm_users表示这个进程地址空间被多少线程共享或者引用,而mm_count则表示这个地址空间被内核线程引用的次数+1。
比如一个进程A有3个线程,那么这个A的mm_struct的mm_users值为3,但是mm_count为1,所以mm_count是process level的counter。维护2个counter有何用处呢?考虑这样的scenario,内核调度完A以后,切换到内核内核线程B,B ’borrow' A的mm描述符以访问内核空间,这时mm_count变成了2,同时另外一个cpu core调度了A并且进程A exit,这个时候mm_users变为了0,mm_count变为了1,但是内核不会因为mm_users0而销毁这个mm_struct,内核只会当mm_count0的时候才会释放mm_struct,因为这个时候既没有用户进程使用这个地址空间,也没有内核线程引用这个地址空间
449static struct mm_struct * mm_init(struct mm_struct * mm, struct task_struct *p)
450{
451 atomic_set(&mm->mm_users, 1);
452 atomic_set(&mm->mm_count, 1);
在初始化一个mm实例的时候,mm_users和mm_count都被初始化为1。
2994/*
2995 * context_switch - switch to the new MM and the new
2996 * thread's register state.
2997 */
2998static inline void
2999context_switch(struct rq *rq, struct task_struct *prev,
3000 struct task_struct *next)
3001{
3002 struct mm_struct *mm, *oldmm;
3003
3004 prepare_task_switch(rq, prev, next);
3005 trace_sched_switch(rq, prev, next);
3006 mm = next->mm;
3007 oldmm = prev->active_mm;
3014
3015 if (unlikely(!mm)) {
3016 next->active_mm = oldmm;
3017 atomic_inc(&oldmm->mm_count);
3018 enter_lazy_tlb(oldmm, next);
3019 } else
3020 switch_mm(oldmm, mm, next);
3021
上面的代码是Linux scheduler进行的context switch的一小段,从unlike(!mm)开始,next->active_mm = oldmm表示如果将要切换倒内核线程,则‘借用’前一个拥护进程的mm描述符,并把他赋给active_mm,重点是将‘借用’的mm描述符的mm_counter加1。
下面我们看看在fork一个进程的时候,是怎样处理的mm_struct的。
1362/*
1363 * Ok, this is the main fork-routine.
1364 *
1365 * It copies the process, and if successful kick-starts
1366 * it and waits for it to finish using the VM if required.
1367 */
1368long do_fork(unsigned long clone_flags,
1369 unsigned long stack_start,
1370 struct pt_regs *regs,
1371 unsigned long stack_size,
1372 int __user *parent_tidptr,
1373 int __user *child_tidptr)
1374{
1417 p = copy_process(clone_flags, stack_start, regs, stack_size,
1418 child_tidptr, NULL, trace);
do_fork调用copy_process。
973/*
974 * This creates a new process as a copy of the old one,
975 * but does not actually start it yet.
976 *
977 * It copies the registers, and all the appropriate
978 * parts of the process environment (as per the clone
979 * flags). The actual kick-off is left to the caller.
980 */
981static struct task_struct *copy_process(unsigned long clone_flags,
982 unsigned long stack_start,
983 struct pt_regs *regs,
984 unsigned long stack_size,
985 int __user *child_tidptr,
986 struct pid *pid,
987 int trace)
988{
1155 if ((retval = copy_mm(clone_flags, p)))
1156 goto bad_fork_cleanup_signal;
copy_process调用copy_mm,下面来分析copy_mm。
681static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
682{
683 struct mm_struct * mm, *oldmm;
684 int retval;
685
686 tsk->min_flt = tsk->maj_flt = 0;
687 tsk->nvcsw = tsk->nivcsw = 0;
688#ifdef CONFIG_DETECT_HUNG_TASK
689 tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw;
690#endif
691
692 tsk->mm = NULL;
693 tsk->active_mm = NULL;
694
695 /*
696 * Are we cloning a kernel thread?
697 *
698 * We need to steal a active VM for that..
699 */
700 oldmm = current->mm;
701 if (!oldmm)
702 return 0;
703
704 if (clone_flags & CLONE_VM) {
705 atomic_inc(&oldmm->mm_users);
706 mm = oldmm;
707 goto good_mm;
708 }
709
710 retval = -ENOMEM;
711 mm = dup_mm(tsk);
712 if (!mm)
713 goto fail_nomem;
714
715good_mm:
716 /* Initializing for Swap token stuff */
717 mm->token_priority = 0;
718 mm->last_interval = 0;
719
720 tsk->mm = mm;
721 tsk->active_mm = mm;
722 return 0;
723
724fail_nomem:
725 return retval;
726}
692,693行,对子进程或者线程的mm和active_mm初始化(NULL)。
700 - 708行,就是我们上面说的如果是创建线程,则新线程共享创建进程的mm,所以不需要进行下面的copy操作。
重点就是711行的dup_mm(tsk)。
621/*
622 * Allocate a new mm structure and copy contents from the
623 * mm structure of the passed in task structure.
624 */
625struct mm_struct *dup_mm(struct task_struct *tsk)
626{
627 struct mm_struct *mm, *oldmm = current->mm;
628 int err;
629
630 if (!oldmm)
631 return NULL;
632
633 mm = allocate_mm();
634 if (!mm)
635 goto fail_nomem;
636
637 memcpy(mm, oldmm, sizeof(*mm));
638
639 /* Initializing for Swap token stuff */
640 mm->token_priority = 0;
641 mm->last_interval = 0;
642
643 if (!mm_init(mm, tsk))
644 goto fail_nomem;
645
646 if (init_new_context(tsk, mm))
647 goto fail_nocontext;
648
649 dup_mm_exe_file(oldmm, mm);
650
651 err = dup_mmap(mm, oldmm);
652 if (err)
653 goto free_pt;
654
655 mm->hiwater_rss = get_mm_rss(mm);
656 mm->hiwater_vm = mm->total_vm;
657
658 if (mm->binfmt && !try_module_get(mm->binfmt->module))
659 goto free_pt;
660
661 return mm;
633行,用slab分配了mm_struct的内存对象。
637行,对子进程的mm_struct进程赋值,使其等于父进程,这样子进程mm和父进程mm的每一个域的值都相同。
在copy_mm的实现中,主要是为了实现unix COW的语义,所以理论上我们只需要父子进程mm中的start_x和end_x之类的域(像start_data,end_data)相等,而对其余的域(像mm_users)则需要re-init,这个操作主要在mm_init中完成。
449static struct mm_struct * mm_init(struct mm_struct * mm, struct task_struct *p)
450{
451 atomic_set(&mm->mm_users, 1);
452 atomic_set(&mm->mm_count, 1);
453 init_rwsem(&mm->mmap_sem);
454 INIT_LIST_HEAD(&mm->mmlist);
455 mm->flags = (current->mm) ?
456 (current->mm->flags & MMF_INIT_MASK) : default_dump_filter;
457 mm->core_state = NULL;
458 mm->nr_ptes = 0;
459 set_mm_counter(mm, file_rss, 0);
460 set_mm_counter(mm, anon_rss, 0);
461 spin_lock_init(&mm->page_table_lock);
462 mm->free_area_cache = TASK_UNMAPPED_BASE;
463 mm->cached_hole_size = ~0UL;
464 mm_init_aio(mm);
465 mm_init_owner(mm, p);
466
467 if (likely(!mm_alloc_pgd(mm))) {
468 mm->def_flags = 0;
469 mmu_notifier_mm_init(mm);
470 return mm;
471 }
472
473 free_mm(mm);
474 return NULL;
475}
其中特别要关注的是467 - 471行的mm_alloc_pdg,也就是page table的拷贝,page table负责logic address到physical address的转换。
拷贝的结果就是父子进程有独立的page table,但是page table里面的每个entries值都是相同的,也就是说父子进程独立地址空间中相同logical address都对应于相同的physical address,这样也就是实现了父子进程的COW(copy on write)语义。
事实上,vfork和fork相比,最大的开销节省就是对page table的拷贝。
而在内核2.6中,由于page table的拷贝,fork在性能上是有所损耗的,所以内核社区里面讨论过shared page table的实现(http://lwn.net/Articles/149888/)。
Linux进程地址管理之mm_struct的更多相关文章
- Linux 进程
Linux 进程 在用户空间,进程是由进程标识符(PID)表示的.从用户的角度来看,一个 PID 是一个数字值,可惟一标识一个进程.一个 PID 在进程的整个生命期间不会更改,但 PID 可以在进程销 ...
- Linux进程管理之task_struct结构
转载:http://blog.csdn.net/npy_lp/article/details/7335187 内核源码:linux-2.6.38.8.tar.bz2 目标平台:ARM体系结构 进程是处 ...
- Linux进程管理知识整理
Linux进程管理知识整理 1.进程有哪些状态?什么是进程的可中断等待状态?进程退出后为什么要等待调度器删除其task_struct结构?进程的退出状态有哪些? TASK_RUNNING(可运行状态) ...
- linux 进程地址空间的一步步探究
我们知道,在32位机器上linux操作系统中的进程的地址空间大小是4G,其中0-3G是用户空间,3G-4G是内核空间.其实,这个4G的地址空间是不存在的,也就是我们所说的虚拟内存空间. 那虚拟内存空间 ...
- linux进程模型总结
Linux进程通过一个task_struct结构体描述,在linux/sched.h中定义,通过理解该结构,可更清楚的理解linux进程模型. 包含进程所有信息的task_struct数据 ...
- linux 进程(一)---基本概念
一.进程的定义 进程是操作系统的概念,每当我们执行一个程序时,对于操作系统来讲就创建了一个进程,在这个过程中,伴随着资源的分配和释放.可以认为进程是一个程序的一次执行过程. 二.进 ...
- Linux进程模型
----原文链接:http://www.cnblogs.com/biyeymyhjob/archive/2012/08/01/2617884.html------ Linux进程通过一个task_st ...
- Linux进程上下文切换过程context_switch详解--Linux进程的管理与调度(二十一)
1 前景回顾 1.1 Linux的调度器组成 2个调度器 可以用两种方法来激活调度 一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU 另一种是通过周期性的机制, 以固定的频率运行, 不时的检测 ...
- Linux下0号进程的前世(init_task进程)今生(idle进程)----Linux进程的管理与调度(五)【转】
前言 Linux下有3个特殊的进程,idle进程(PID = 0), init进程(PID = 1)和kthreadd(PID = 2) idle进程由系统自动创建, 运行在内核态 idle进程其pi ...
随机推荐
- Mysql学习总结(38)——21条MySql性能优化经验
今天,数据库的操作越来越成为整个应用的性能瓶颈了,这点对于Web应用尤其明显.关于数据库的性能,这并不只是DBA才需要担心的事,而这更是我们程序员需要去关注的事情. 当我们去设计数据库表结构,对操作数 ...
- Spring Boot-全局异常处理(八)
SpringBoot默认异常默认处理机制 Spring boot错误异常时通过BasicErrorController来处理的 通过判断是浏览器请求还是ajax请求响应页面或者json BasicEr ...
- FADE:云存储中数据安全删除
FADE: Secure Overlay Cloud Storage with File Assured Deletion ,论文发表于SecureComm,C类. 香港大学的研究 ...
- java.lang.Object 方法解析
1.clone() a.是一个native方法,效率比非native高 b.是protected 修饰的,要用他必须继承object,默认都是继承object的 c.返回是一个object 对象,需要 ...
- LAS文件转TXT文件
LAS文件转TXT文件 https://www.liblas.org/start.html
- PixelUtils:像素转换工具
/** 像素转换工具 */ public class PixelUtils { /** * The context. */ private static Context mContext = Cust ...
- 状态压缩dp poj 3254 hdu5045
近来感觉状态压缩dp的强大性(灵活利用了二进制运算非常关键). . . 于是做了俩提来看看..毕竟队友是专业的dp.我仅仅是管中窥豹下而已.. 日后有机会再与之玩耍玩耍...ps:假设上天再给我一次机 ...
- Linux 获取设备树源文件(DTS)里的资源【转】
本文转载自:http://blog.csdn.net/keleming1/article/details/51036000 http://www.cnblogs.com/dyllove98/archi ...
- 理解和配置 Linux 下的 OOM Killer【转】
本文转载自:http://www.vpsee.com/2013/10/how-to-configure-the-linux-oom-killer/ 最近有位 VPS 客户抱怨 MySQL 无缘无故挂掉 ...
- Opencv绘制最小外接矩形、最小外接圆
Opencv中求点集的最小外结矩使用方法minAreaRect,求点集的最小外接圆使用方法minEnclosingCircle. minAreaRect方法原型: RotatedRect minAre ...