進程地址空間
- 1 內存描述符
- 分配內存描述符
- 銷毀內存描述符
- mm_struct與內核線程
- 2 內存區域
- VMA標志
- VMA操作
- 內存區域的樹形結構和內存區域的鏈表結構
- 3 操作內存區域
- find_vma()
- find_vma_prev()
- find_vma_intersection()
- 4 mmap()和do_mmap():創建地址空間
- mmap() 系統調用
- 5 munmap()和do_munmap():刪除地址空間
- munmap()系統調用
- 6 頁表
內核除了管理本身的內存外,還必須管理進程的地址空間,也就是系統中每個用戶空間地址所對應的內存。Linux操作系統采用虛擬內存技術,因此,系統中的所有進程之間以虛擬方法共享內存,對每個進程來說,它們好像都可以訪問整個系統的所有物理內存。
進程地址空間由每個進程中的線性地址區組成,每個進程都有一個32位或64位的平坦地址空間,空間的具體大小取決于體系結構,平坦地址空間是指地址空間范圍是一個獨立的連續空間(比如,地址從0擴展到429496729位地址空間)。一些操作系統提供了段地址空間,這種地址空間并非是一個獨立的線性區域,而是被分段的,但現代采用虛擬內存的操作系統通常都使用平坦地址空間而不是分段式的內存模式。通常情況下,每個進程都有唯一的這種平坦地址空間,而進程地址空間之間彼此互不相干,兩個不同的進程可以在它們各自地址空間的相同地址內存放不同的數據。進程之間也可以選擇共享地址空間,我們稱這樣的進程為線程。
1 內存描述符
內核使用內存描述符結構體表示進程的地址空間,該結構包含了和進程地址空間有關的全部信息。內存描述符由mm_struct結構體表示,定義在文件linux/sched.h中。
struct mm_struct {struct vm_area_struct * mmap; /* list of VMAs */struct rb_root mm_rb;struct vm_area_struct * mmap_cache; /* last find_vma result */unsigned long (*get_unmapped_area) (struct file *filp,unsigned long addr, unsigned long len,unsigned long pgoff, unsigned long flags);void (*unmap_area) (struct vm_area_struct *area);unsigned long mmap_base; /* base of mmap area */unsigned long free_area_cache; /* first hole */pgd_t * pgd;atomic_t mm_users; /* How many users with user space? */atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */int map_count; /* number of VMAs */struct rw_semaphore mmap_sem;spinlock_t page_table_lock; /* Protects page tables, mm->rss, mm->anon_rss */struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung* together off init_mm.mmlist, and are protected* by mmlist_lock*/unsigned long start_code, end_code, start_data, end_data;unsigned long start_brk, brk, start_stack;unsigned long arg_start, arg_end, env_start, env_end;unsigned long rss, anon_rss, total_vm, locked_vm, shared_vm;unsigned long exec_vm, stack_vm, reserved_vm, def_flags, nr_ptes;unsigned long saved_auxv[42]; /* for /proc/PID/auxv */unsigned dumpable:1;cpumask_t cpu_vm_mask;/* Architecture-specific MM context */mm_context_t context;/* Token based thrashing protection. */unsigned long swap_token_time;char recent_pagein;/* coredumping support */int core_waiters;struct completion *core_startup_done, core_done;/* aio bits */rwlock_t ioctx_list_lock;struct kioctx *ioctx_list;struct kioctx default_kioctx;
};
mm_users域記錄正在使用該地址的進程數目。比如,有兩個進程共享該地址空間,那么mm_users的值便等于2;mm_count是mm_struct的主引用計數,只要mm_users不為0,那么mm_count值就等于1。當mm_users的值減為0時,mm_count域的值才為0,如果mm_count的值等于0,說明已經沒有任何指向該mm_struct結構體的引用了,這個時候該結構體會被銷毀。
mmap和mm_rb這兩個數據結構描述的對象是相同的:該地址空間中的全部內存區域。但是mmap是以鏈表形式存放而后者以紅-黑樹形式存放。mmap結構體作為鏈表,利于簡單、高效地遍歷所有元素,而mm_rb結構體更適合搜索指定元素。
所有的mm_struct結構體都通過自身的mmlist域連接在一個雙向鏈表中,該鏈表的首元素是init_mm內存描述符,它代表init進程的地址空間,內存描述符的總數存放在mmlist_nr全局變量中,該變量定義在kernel/fork.c中。
分配內存描述符
在進程的struct task_struct
進程描述符中,mm域存放著該進程使用的內存描述符,所以current->mm便指向當前進程的內存描述符。fork函數利用copy_mm()函數復制父進程的內存描述符,也就是current->mm域給其子進程,而子進程中的mm_struct結構體實際是通過文件kernel/fork.c中的allocate_mm()宏從mm_cachep slab緩存中分配得到的。
如果父進程希望和其子進程共享地址空間,可以在調用clone()時,設置CLONE_VM標志。我們把這樣的進程稱作線程。當CLONE_VM被指定后,內核就不再需要調用allocate_mm()函數了,而僅僅需要在調用copy_mm()函數中將mm域指向其父進程的內存描述符就可以了。
銷毀內存描述符
當進程退出時,內核會調用exit_mm函數,該函數執行一些常規的銷毀工作,同時更新一些統計量。其中,該函數會調用mmput()函數減少內存描述符的mm_users用戶計數,如果mm_users降到0,繼續調用mmdrop()函數,減少mm_count,如果mm_count也等于0了,說明該內存描述符不再有任何使用者了,那么調用free_mm宏通過kmem_cache_free()將mm_struct結構體歸還到mm_cachep_slab緩存中。
mm_struct與內核線程
內核線程沒有進程地址空間,也沒有相關的內存描述符,所以內核線程對應的進程描述符中mm域為空。
當一個進程被調度時,該進程的mm域指向的地址空間被裝載到內存,進程描述符中的active_mm域會被更新,指向新的地址空間。內核線程沒有地址空間,所以mm域為NULL,于是當一個內核線程被調用時,就會保留前一個進程的地址空間,隨后內核更新內核線程對應的進程描述符中的active_mm域,使其指向前一個進程的內存描述符。
2 內存區域
內存區域由vm_area_struct結構體描述,定義在文件linux/mm.h中,內存區域在內核中也經常被稱作虛擬內存區域或VMA。
vm_area_struct結構體描述了指定地址空間內連續區間上的一個獨立內存范圍。內核將每個內存區域作為一個單獨的內存對象管理,每個內存區域都擁有一致的屬性,
struct vm_area_struct {struct mm_struct * vm_mm; /* The address space we belong to. */unsigned long vm_start; /* Our start address within vm_mm. */unsigned long vm_end; /* The first byte after our end addresswithin vm_mm. *//* linked list of VM areas per task, sorted by address */struct vm_area_struct *vm_next;pgprot_t vm_page_prot; /* Access permissions of this VMA. */unsigned long vm_flags; /* Flags, listed below. */struct rb_node vm_rb;/** For areas with an address space and backing store,* linkage into the address_space->i_mmap prio tree, or* linkage to the list of like vmas hanging off its node, or* linkage of vma in the address_space->i_mmap_nonlinear list.*/union {struct {struct list_head list;void *parent; /* aligns with prio_tree_node parent */struct vm_area_struct *head;} vm_set;struct prio_tree_node prio_tree_node;} shared;/** A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma* list, after a COW of one of the file pages. A MAP_SHARED vma* can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack* or brk vma (with NULL file) can only be in an anon_vma list.*/struct list_head anon_vma_node; /* Serialized by anon_vma->lock */struct anon_vma *anon_vma; /* Serialized by page_table_lock *//* Function pointers to deal with this struct. */struct vm_operations_struct * vm_ops;/* Information about our backing store: */unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZEunits, *not* PAGE_CACHE_SIZE */struct file * vm_file; /* File we map to (can be NULL). */void * vm_private_data; /* was vm_pte (shared mem) */#ifdef CONFIG_NUMAstruct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
};
每個內存描述符都對應于進程地址空間的唯一區間,vm_start域指向區間的首地址,vm_end域指向區間的尾地址之后的第一個字節,vm_end~vm_start的大小便是內存區間的長度,內存區域的位置就在[vm_start,vm_end]之中,注意,在同一個地址空間內的不同內存區間不能重疊。
vm_mm域指向和VMA相關的mm_struct結構體,注意每個VMA對其相關的mm_struct來說都是唯一的,所以即使兩個獨立的進程將同一個文件映射到各自的地址空間,它們分別都會有一個vm_area_struct結構體標志自己的內存區域,但是如果兩個線程共享一個地址空間,那么它們也同時共享其中所有的vm_area_struct結構體。
VMA標志
VMA標志是一種位標志,其定義在linux/mm.h中,它包含在vm_flags域內,標志了內存區域所包含的頁面的行為和信息,和物理頁的訪問權限不同,VMA標志反映了內核處理頁面所需要遵守的行為準則,而不是硬件要求。
VMA操作
vm_area_struct結構體中的vm_ops指向與指定內存區域相關的操作函數表,內核使用表中的方法操作VMA。vm_area_struct作為通用對象代表了任何類型的內存區域,而操作表描述針對特定的對象實例的特定方法。
操作函數表由vm_operations_struct結構體表示,定義在文件linux/mm.h中
/** These are the virtual MM functions - opening of an area, closing and* unmapping it (needed to keep files on disk up-to-date etc), pointer* to the functions called when a no-page or a wp-page exception occurs. */
struct vm_operations_struct {void (*open)(struct vm_area_struct * area);void (*close)(struct vm_area_struct * area);struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, int *type);int (*populate)(struct vm_area_struct * area, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock);
#ifdef CONFIG_NUMAint (*set_policy)(struct vm_area_struct *vma, struct mempolicy *new);struct mempolicy *(*get_policy)(struct vm_area_struct *vma,unsigned long addr);
#endif
};
內存區域的樹形結構和內存區域的鏈表結構
上面說過,可以通過內存描述符中的mmap和mm_rb域之一訪問內存區域,這兩個域各自獨立地指向與內存描述符相關的全部內存區域對象vm_area_struct。
mmap使用單獨鏈表連接所有的內存區域對象vm_area_struct,每一個vm_area_struct結構體通過自身的vm_next域被連入鏈表,所有的區域按地址增長的方向排序,mmap域指向鏈表中第一個內存區域,鏈中最后一個VMA結構體指針指向空。
mm_rb域使用紅-黑樹連接所有內存區域對象,mm_rb域指向紅-黑樹的根結點,地址空間中每一個vm_area_struct結構體通過自身的vm_rb域連接到樹中。
鏈表用于需要遍歷全部結點的時候,而紅-黑樹適用于在地址空間中定位特定內存區域的時候。內核為了內存區域上的各種不同操作都能獲得高性能,所以同時使用了這兩種數據結構。
3 操作內存區域
內核定義了許多內存區域操作函數,它們都聲明在文件linux/mm.h中
find_vma()
find_vma()函數定義在mm/mmap.c中。
/* Look up the first VMA which satisfies addr < vm_end, NULL if none. */
extern struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr);
該函數在指定的地址空間中搜索第一個vm_end大于addr的內存區域。換句話說,該函數尋找第一個包含addr或首地址大于addr的內存區域,如果沒有發現這樣的區域,該函數返回NULL。否則返回指向匹配的內存區域的vm_area_struct結構體指針,返回的結構會被緩存在內存描述符的mmap_cache域中,所以find_vma會先在緩存中查找,如果指定的地址不在緩存中,那么必須搜搜和內存描述符相關的所有內存區域,這種搜索通過紅-黑樹進行。
find_vma_prev()
find_vma_prev()函數和find_vma()工作方式相同,但是它返回第一個小于addr的VMA。該函數定義和聲明分別在文件mm/mmap.c中和文件linux/mm.h中
extern struct vm_area_struct * find_vma_prev(struct mm_struct * mm, unsigned long addr,struct vm_area_struct **pprev)
pprev參數存放指向先于addr的VMA指針。
find_vma_intersection()
find_vma_intersection()返回第一個和指定地址區間相交的VMA。因為該函數和內聯函數,所以定義在文件linux/mm.h中:
static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr)
{struct vm_area_struct * vma = find_vma(mm,start_addr);if (vma && end_addr <= vma->vm_start)vma = NULL;return vma;
}
第一個參數是要搜索的地址空間,start_addr是區間的開始首位置,end_addr是區間的尾位置,
4 mmap()和do_mmap():創建地址空間
內核使用do_mmap()函數創建一個新的線性地址區間。如果創建的地址區間和一個已經存在的地址區間相鄰,并且它們具有相同的訪問權限的話,那么兩個區間將合并為一個。do_mmap()函數會將一個地址區間加入到進程的地址空間中。
do_mmap()函數定義在linux/mm.h中
static inline 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,那么就代碼這次映射沒有和文件相關,該情況被稱作匿名映射,如果指定了文件名和偏移量,那么該映射被稱為文件映射。
addr是可選參數,它指定搜索空閑區域的起始位置。
prot參數指定內存區域中頁面的訪問權限。訪問權限標志定義在文件asm/mman.h中。
flag參數指定了VMA標志,這些標志也定義在文件asm/mman.h中
mmap() 系統調用
在用戶空間可以通過mmap()系統調用獲取內核函數do_mmap()的功能。
5 munmap()和do_munmap():刪除地址空間
do_munmap()函數從特定的進程地址空間中刪除指定地址區間,該函數定義在文件linux/mm.h中:
extern int do_munmap(struct mm_struct *mm, unsigned long start, size_t len);
第一個參數指定要刪除區域所在的地址空間,刪除從地址start開始,長度為len字節的地址區間,如果成功,返回0.
munmap()系統調用
系統調用munmap()給用戶空間程序提供了一種從自身地址空間刪除指定區間的方法。
int munmap(void *start ,size_t length)
該系統調用定義在mm/mmap.c中,它是對do_munmap的一個簡單封裝
6 頁表
雖然應用程序操作的對象是映射到物理內存之上的虛擬內存,但是處理器直接操作的卻是物理內存,所以當應用程序訪問一個虛擬地址時,首先必須將虛擬地址轉化為物理地址,然后處理器才能解析地址訪問請求。地址的轉換工作是通過查詢頁表完成的。
頁表對應的結構體依賴具體的體系結構,所以定義在文件asm/page.h中