1. 程式人生 > >linux 內存地址空間管理 mm_struct

linux 內存地址空間管理 mm_struct

clone mod ppr head actual rom __user 虛擬 tom

http://blog.csdn.net/yusiguyuan/article/details/39520933

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{
 683        struct mm_struct * mm, *oldmm;
 684        int retval;
 
 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        }
技術分享

無論我們在調用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_users==0而銷毀這個mm_struct,內核只會當mm_count==0的時候才會釋放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