天天看點

一篇讀懂Linux核心-核心位址空間分布和程序位址空間

作者:linux上的碼農

核心位址空間分布

一篇讀懂Linux核心-核心位址空間分布和程式位址空間

直接映射區:線性空間中從3G開始最大896M的區間,為直接記憶體映射區,該區域的線性位址和實體位址存線上性轉換關系:線性位址=3G+實體位址。

動态記憶體映射區:該區域由核心函數vmalloc來配置設定,特點是:線性空間連續,但是對應的實體空間不一定連續。vmalloc配置設定的線性位址所對應的實體頁可能處于低端記憶體,也可能處于高端記憶體。

永久記憶體映射區:該區域可通路高端記憶體。通路方法是使用alloc_page(_GFP_HIGHMEM)配置設定高端記憶體頁或者使用kmap函數将配置設定到的高端記憶體映射到該區域。

固定映射區:該區域和4G的頂端隻有4k的隔離帶,其每個位址項都服務于特定的用途,如ACPI_BASE等。

程序的位址空間

linux采用虛拟記憶體管理技術,每一個程序都有一個3G大小的獨立的程序位址空間,這個位址空間就是使用者空間。每個程序的使用者空間都是完全獨立、互不相幹的。程序通路核心空間的方式:系統調用和中斷。

建立程序等程序相關操作都需要配置設定記憶體給程序。這時程序申請和獲得的不是實體位址,僅僅是虛拟位址。

實際的實體記憶體隻有當程序真的去通路新擷取的虛拟位址時,才會由“請頁機制”産生“缺頁”異常,進而進入配置設定實際頁框的程式。該異常是虛拟記憶體機制賴以存在的基本保證,它會告訴核心去為程序配置設定實體頁,并建立對應的頁表,這之後虛拟位址才實實在在的映射到了實體位址上。

一篇讀懂Linux核心-核心位址空間分布和程式位址空間

vmalloc和kmalloc差別

1,kmalloc對應于kfree,配置設定的記憶體處于3GB~high_memory之間,這段核心空間與實體記憶體的映射一一對應,可以配置設定連續的實體記憶體; vmalloc對應于vfree,配置設定的記憶體在VMALLOC_START~4GB之間,配置設定連續的虛拟記憶體,但是實體上不一定連續。

2,vmalloc() 配置設定的實體位址無需連續,而kmalloc() 確定頁在實體上是連續的

3,kmalloc配置設定記憶體是基于slab,是以slab的一些特性包括着色,對齊等都具備,性能較好。實體位址和邏輯位址都是連續的。

4,最主要的差別是配置設定大小的問題,比如你需要28個位元組,那一定用kmalloc,如果用vmalloc,配置設定不多次機器就罷工了。

盡管僅僅在某些情況下才需要實體上連續的記憶體塊,但是,很多核心代碼都調用kmalloc(),而不是用vmalloc()獲得記憶體。這主要是出于性能的考慮。vmalloc()函數為了把實體上不連續的頁面轉換為虛拟位址空間上連續的頁,必須專門建立頁表項。還有,通過 vmalloc()獲得的頁必須一個一個的進行映射(因為它們實體上不是連續的),這就會導緻比直接記憶體映射大得多的緩沖區重新整理。因為這些原因,vmalloc()僅在絕對必要時才會使用,最典型的就是為了獲得大塊記憶體時,例如,當子產品被動态插入到核心中時,就把子產品裝載到由vmalloc()配置設定的記憶體上。

更多linux核心視訊教程文檔資料免費領取背景私信【核心】自行擷取.

一篇讀懂Linux核心-核心位址空間分布和程式位址空間
一篇讀懂Linux核心-核心位址空間分布和程式位址空間

Linux核心源碼/記憶體調優/檔案系統/程序管理/裝置驅動/網絡協定棧-學習視訊教程-騰訊課堂

程序位址空間

前邊我已經說過了核心是如何管理實體記憶體。但事實是核心是作業系統的核心,不光管理本身的記憶體,還要管理程序的位址空間。linux作業系統采用虛拟記憶體技術,所有程序之間以虛拟方式共享記憶體。程序位址空間由每個程序中的線性位址區組成,而且更為重要的特點是核心允許程序使用該空間中的位址。通常情況況下,每個程序都有唯一的位址空間,而且程序位址空間之間彼此互不相幹。但是程序之間也可以選擇共享位址空間,這樣的程序就叫做線程。

核心使用記憶體描述符結構表示程序的位址空間,由結構體mm_struct結構體表示,定義在linux/sched.h中,如下:

struct mm_struct {
        struct vm_area_struct  *mmap;               /* list of memory areas */
        struct rb_root         mm_rb;               /* red-black tree of VMAs */
        struct vm_area_struct  *mmap_cache;         /* last used memory area */
        unsigned long          free_area_cache;     /* 1st address space hole */
        pgd_t                  *pgd;                /* page global directory */
        atomic_t               mm_users;            /* address space users */
        atomic_t               mm_count;            /* primary usage counter */
        int                    map_count;           /* number of memory areas */
        struct rw_semaphore    mmap_sem;            /* memory area semaphore */
        spinlock_t             page_table_lock;     /* page table lock */
        struct list_head       mmlist;              /* list of all mm_structs */
        unsigned long          start_code;          /* start address of code */
        unsigned long          end_code;            /* final address of code */
        unsigned long          start_data;          /* start address of data */
        unsigned long          end_data;            /* final address of data */
        unsigned long          start_brk;           /* start address of heap */
        unsigned long          brk;                 /* final address of heap */
        unsigned long          start_stack;         /* start address of stack */
        unsigned long          arg_start;           /* start of arguments */
        unsigned long          arg_end;             /* end of arguments */
        unsigned long          env_start;           /* start of environment */
        unsigned long          env_end;             /* end of environment */
        unsigned long          rss;                 /* pages allocated */
        unsigned long          total_vm;            /* total number of pages */
        unsigned long          locked_vm;           /* number of locked pages */
        unsigned long          def_flags;           /* default access flags */
        unsigned long          cpu_vm_mask;         /* lazy TLB switch mask */
        unsigned long          swap_address;        /* last scanned address */
        unsigned               dumpable:1;          /* can this mm core dump? */
        int                    used_hugetlb;        /* used hugetlb pages? */
        mm_context_t           context;             /* arch-specific data */
        int                    core_waiters;        /* thread core dump waiters */
        struct completion      *core_startup_done;  /* core start completion */
        struct completion      core_done;           /* core end completion */
        rwlock_t               ioctx_list_lock;     /* AIO I/O list lock */
        struct kioctx          *ioctx_list;         /* AIO I/O list */
        struct kioctx          default_kioctx;      /* AIO default I/O context */
};
           

mm_users記錄了正在使用該位址的程序數目(比如有兩個程序在使用,那就為2)。mm_count是該結構的主引用計數,隻要mm_users不為0,它就為1。但其為0時,後者就為0。這時也就說明再也沒有指向該mm_struct結構體的引用了,這時該結構體會被銷毀。核心之是以同時使用這兩個計數器是為了差別主使用計數器和使用該位址空間的程序的數目。mmap和mm_rb描述的都是同一個對象:該位址空間中的全部記憶體區域。不同隻是前者以連結清單,後者以紅黑樹的形式組織。所有的mm_struct結構體都通過自身的mmlist域連接配接在一個雙向連結清單中,該連結清單的首元素是init_mm記憶體描述符,它代表init程序的位址空間。另外需要注意,操作該連結清單的時候需要使用mmlist_lock鎖來防止并發通路,該鎖定義在檔案kernel/fork.c中。記憶體描述符的總數在mmlist_nr全局變量中,該變量也定義在檔案fork.c中。

我前邊說過的程序描述符中有一個mm域,這裡邊存放的就是該程序使用的記憶體描述符,通過current->mm便可以指向目前程序的記憶體描述符。fork函數利用copy_mm()函數就實作了複制父程序的記憶體描述符,而子程序中的mm_struct結構體實際是通過檔案kernel/fork.c中的allocate_mm()宏從mm_cachep slab緩存中配置設定得到的。通常,每個程序都有唯一的mm_struct結構體。

前邊也說過,在linux中,程序和線程其實是一樣的,唯一的不同點就是是否共享這裡的位址空間。這個可以通過CLONE_VM标志來實作。linux核心并不差別對待它們,線程對核心來說僅僅是一個共向特定資源的程序而已。好了,如果你設定這個标志了,似乎很多問題都解決了。不再要allocate_mm函數了,前邊剛說作用。而且在copy_mm()函數中将mm域指向其父程序的記憶體描述符就可以了,如下:

if (clone_flags & CLONE_VM) {
        /*
         * current is the parent process and
         * tsk is the child process during a fork()
         */
         atomic_inc(¤t->mm->mm_users);
         tsk->mm = current->mm;
}
           

最後,當程序退出的時候,核心調用exit_mm()函數,這個函數調用mmput()來減少記憶體描述符中的mm_users使用者計數。如果計數降為0,繼續調用mmdrop函數,減少mm_count使用計數。如果使用計數也為0,則調用free_mm()宏通過kmem_cache_free()函數将mm_struct結構體歸還到mm_cachep slab緩存中。

但對于核心而言,核心線程沒有程序位址空間,也沒有相關的記憶體描述符,核心線程對應的程序描述符中mm域也為空。但核心線程還是需要使用一些資料的,比如頁表,為了避免核心線程為記憶體描述符和頁表浪費記憶體,也為了當新核心線程運作時,避免浪費處理器周期向新位址空間進行切換,核心線程将直接使用前一個程序的記憶體描述符。回憶一下我剛說的程序排程問題,當一個程序被排程時,程序結構體中mm域指向的位址空間會被裝載到記憶體,程序描述符中的active_mm域會被更新,指向新的位址空間。但我們這裡的核心是沒有mm域(為空),是以,當一個核心線程被排程時,核心發現它的mm域為NULL,就會保留前一個程序的位址空間,随後核心更新核心線程對應的程序描述符中的active域,使其指向前一個程序的記憶體描述符。是以在需要的時候,核心線程便可以使用前一個程序的頁表。因為核心線程不妨問使用者空間的記憶體,是以它們僅僅使用位址空間中和核心記憶體相關的資訊,這些資訊的含義和普通程序完全相同。

記憶體區域由vm_area_struct結構體描述,定義在linux/mm.h中,記憶體區域在核心中也經常被稱作虛拟記憶體區域或VMA.它描述了指定位址空間内連續區間上的一個獨立記憶體範圍。核心将每個記憶體區域作為一個單獨的記憶體對象管理,每個記憶體區域都擁有一緻的屬性。結構體如下:

struct vm_area_struct {
        struct mm_struct             *vm_mm;        /* associated mm_struct */
        unsigned long                vm_start;      /* VMA start, inclusive */
        unsigned long                vm_end;        /* VMA end , exclusive */
        struct vm_area_struct        *vm_next;      /* list of VMA's */
        pgprot_t                     vm_page_prot;  /* access permissions */
        unsigned long                vm_flags;      /* flags */
        struct rb_node               vm_rb;         /* VMA's node in the tree */
        union {         /* links to address_space->i_mmap or i_mmap_nonlinear */
                struct {
                        struct list_head        list;
                        void                    *parent;
                        struct vm_area_struct   *head;
                } vm_set;
                struct prio_tree_node prio_tree_node;
        } shared;
        struct list_head             anon_vma_node;     /* anon_vma entry */
        struct anon_vma              *anon_vma;         /* anonymous VMA object */
        struct vm_operations_struct  *vm_ops;           /* associated ops */
        unsigned long                vm_pgoff;          /* offset within file */
        struct file                  *vm_file;          /* mapped file, if any */
        void                         *vm_private_data;  /* private data */
};
    
           

每個記憶體描述符都對應于位址程序空間中的唯一區間。vm_mm域指向和VMA相關的mm_struct結構體。兩個獨立的程序将同一個檔案映射到各自的位址空間,它們分别都會有一個vm_area_struct結構體來标志自己的記憶體區域;但是如果兩個線程共享一個位址空間,那麼它們也同時共享其中的所有vm_area_struct結構體。

在上面的vm_flags域中存放的是VMA标志,标志了記憶體區域所包含的頁面的行為和資訊,反映了核心處理頁面所需要遵循的行為準則,如下表下述:

一篇讀懂Linux核心-核心位址空間分布和程式位址空間

上表已經相當詳細了,而且給出了說明,我就不說了。在vm_area_struct結構體中的vm_ops域指向域指定記憶體區域相關的操作函數表,核心使用表中的方法操作VMA。vm_area_struct作為通用對象代表了任何類型的記憶體區域,而操作表描述針對特定的對象執行個體的特定方法。操作函數表由vm_operations_struct結構體表示,定義在linux/mm.h中,如下:

struct vm_operations_struct {
        void (*open) (struct vm_area_struct *);
        void (*close) (struct vm_area_struct *);
        struct page * (*nopage) (struct vm_area_struct *, unsigned long, int);
        int (*populate) (struct vm_area_struct *, unsigned long, unsigned long,pgprot_t, unsigned long, int);
};
           

open:當指定的記憶體區域被加入到一個位址空間時,該函數被調用。

close:當指定的記憶體區域從位址空間删除時,該函數被調用。

nopages:當要通路的頁不在實體記憶體中時,該函數被頁錯誤處理程式調用。

populate:該函數被系統調用remap_pages調用來為将要發生的缺頁中斷預映射一個新映射。

記性好的你一定記得記憶體描述符中的mmap和mm_rb域都獨立地指向與記憶體描述符相關的全體記憶體區域對象。它們包含完全相同的vm_area_struct結構體的指針,僅僅組織方式不同而已。前者以連結清單的方式進行組織,所有的區域按位址增長的方向排序,mmap域指向連結清單中第一個記憶體區域,鍊中最後一個VMA結構體指針指向空。而mm_rb域采用紅--黑樹連接配接所有的記憶體區域對象。它指向紅--黑輸的根節點。位址空間中每一個vm_area_struct結構體通過自身的vm_rb域連接配接到樹中。關于紅黑二叉樹結構我就不細講了,以後可能會詳細說這個問題。核心之是以采用這兩種結構來表示同一記憶體區域,主要是連結清單結構便于周遊所有節點,而紅黑樹結構體便于在位址空間中定位特定記憶體區域的節點。我麼可以使用/proc檔案系統和pmap工具檢視給定程序的記憶體空間和其中所包含的記憶體區域。這裡就不細說了。

核心也為我們提供了對記憶體區域操作的API,定義在linux/mm.h中:

記性好的你一定記得記憶體描述符中的mmap和mm_rb域都獨立地指向與記憶體描述符相關的全體記憶體區域對象。它們包含完全相同的vm_area_struct結構體的指針,僅僅組織方式不同而已。前者以連結清單的方式進行組織,所有的區域按位址增長的方向排序,mmap域指向連結清單中第一個記憶體區域,鍊中最後一個VMA結構體指針指向空。而mm_rb域采用紅--黑樹連接配接所有的記憶體區域對象。它指向紅--黑輸的根節點。位址空間中每一個vm_area_struct結構體通過自身的vm_rb域連接配接到樹中。關于紅黑二叉樹結構我就不細講了,以後可能會詳細說這個問題。核心之是以采用這兩種結構來表示同一記憶體區域,主要是連結清單結構便于周遊所有節點,而紅黑樹結構體便于在位址空間中定位特定記憶體區域的節點。我麼可以使用/proc檔案系統和pmap工具檢視給定程序的記憶體空間和其中所包含的記憶體區域。這裡就不細說了。

核心也為我們提供了對記憶體區域操作的API,定義在linux/mm.h中:

接下來要說的兩個函數就非常重要了,它們負責建立和删除位址空間。

核心使用do_mmap()函數建立一個新的線性位址空間。但如果建立的位址區間和一個已經存在的位址區間相鄰,并且它們具有相同的通路權限的話,那麼兩個區間将合并為一個。如果不能合并,那麼就确實需要建立一個新的vma了,但無論哪種情況,do_mmap()函數都會将一個位址區間加入到程序的位址空間中。這個函數定義在linux/mm.h中,如下:

unsigned long do_mmap(struct file *file, unsigned long addr, unsigned long len, unsigned long prot,unsigned long flag, unsigned long offset)
           

這個函數中由file指定檔案,具體映射的是檔案中從偏移offset處開始,長度為len位元組的範圍内的資料,如果file參數是NULL并且offset參數也是0,那麼就代表這次映射沒有和檔案相關,該情況被稱作匿名映射。如果指定了檔案和偏移量,那麼該映射被稱為檔案映射(file-backed mapping),其中參數prot指定記憶體區域中頁面的通路權限,這些通路權限定義在asm/mman.h中,如下:

一篇讀懂Linux核心-核心位址空間分布和程式位址空間

flag參數指定了VMA标志,這些标志定義在asm/mman.h中,如下:

一篇讀懂Linux核心-核心位址空間分布和程式位址空間

如果系統調用do_mmap的參數中有無效參數,那麼它傳回一個負值;否則,它會在虛拟記憶體中配置設定一個合适的新記憶體區域,如果有可能的話,将新區域和臨近區域進行合并,否則核心從vm_area_cach

ep長位元組緩存中配置設定一個vm_area_struct結構體,并且使用vma_link()函數将新配置設定的記憶體區域添加到位址空間的記憶體區域連結清單和紅黑樹中,随後還要更新記憶體描述符中的total_vm域,然後才傳回新配置設定的位址區間的初始位址。在使用者空間,我們可以通過mmap()系統調用擷取核心函數do_mmap()的功能,這個在unix環境進階程式設計中講的很詳細,我就不好意思繼續說了。我們繼續往下走。

我們說既然有了建立,當然要有删除了,是不?do_mummp()函數就是幹這事的。它從特定的程序位址空間中删除指定位址空間,該函數定義在檔案linux/mm.h中,如下:

int do_munmap(struct mm_struct *mm, unsigned long start, size_t len)
           

第一個參數指定要删除區域所在的位址空間,删除從位址start開始,長度為len位元組的位址空間,如果成功,傳回0,否則傳回負的錯誤碼。與之相對應的使用者空間系統調用是munmap。

下面開始最後一點内容:頁表

我們知道應用程式操作的對象是映射到實體記憶體之上的虛拟記憶體,但是處理器直接操作的确實實體記憶體。是以當應用程式通路一個虛拟位址時,首先必須将虛拟位址轉化為實體位址,然後處理器才能解析位址通路請求。這個轉換工作需要通過查詢頁面才能完成,概括地講,位址轉換需要将虛拟位址分段,使每段虛位址都作為一個索引指向頁表,而頁表項則指向下一級别的頁表或者指向最終的實體頁面。linux中使用三級頁表完成位址轉換。多數體系結構中,搜尋頁表的工作由硬體完成,下表描述了虛拟位址通過頁表找到實體位址的過程:

一篇讀懂Linux核心-核心位址空間分布和程式位址空間

在上面這個圖中,頂級頁表是頁全局目錄(PGD),二級頁表是中間頁目錄(PMD).最後一級是頁表(PTE),該頁表結構指向實體頁。上圖中的頁表對應的結構體定義在檔案asm/page.h中。為了加快查找速度,在linux中實作了快表(TLB),其本質是一個緩沖器,作為一個将虛拟位址映射到實體位址的硬體緩存,當請求通路一個虛拟位址時,處理器将首先檢查TLB中是否緩存了該虛拟位址到實體位址的映射,如果找到了,實體位址就立刻傳回,否則,就需要再通過頁表搜尋需要的實體位址。

- - 核心技術中文網 - 建構全國最權威的核心技術交流分享論壇

轉載位址:一篇讀懂Linux核心-核心位址空間分布和程序位址空間 - 圈點 - 核心技術中文網 - 建構全國最權威的核心技術交流分享論壇

一篇讀懂Linux核心-核心位址空間分布和程式位址空間

繼續閱讀