文章目錄
Linux內存管理第四章 – 進程地址空間(Process Address Space)
Linear Address Space
從user的觀點來看,地址空間是一塊平坦的線性地址空間,但可以預見的是從kernel的觀點來看,地址空間卻大有不同。虛擬地址空間被分割成兩部分,userspace部分隨着進程上下文的切換而改變但kernel space的部分始終保持不變。虛擬地址空間被切割的位置有宏PAGE_OFFSET決定,在x86上PAGE_OFFSET = 0xC0000000。即用戶進程可用的虛擬地址空間是3GB,而另外1GB始終有kernel使用。kernel space的線性虛擬地址的概略圖如下:
從PAGE_OFFSET開始的8MB(兩個PGD所映射的內存空間)預留起來用於加載Linux內核鏡像。對於UMA來說,在kernel image後有很短的間隔之後存放的是全局變量mem_map的地址。而mem_map的地址通常是16MB的位置從而避免使用ZONE_DMA,但也不是總是如此。而NUMA架構下,虛擬mem_map的部分內容將分散在該區域,其具體的位置有各架構決定。例如在X86下,硬件架構指定每個node的lmem_map的地址在數組node_remap_start_vaddr中,然後將node_remap_start_vaddr中的第一個地址賦值給mem_map。
Managing the Address Space
每個進程的進程描述符struct task_struct中的struct mm_struct用來管理用戶虛擬地址空間。
每個地址空間有一系列頁對齊的區域組成,這些區域不會重疊該區域代表一組地址,其中的page是相互關聯的,這些區域用struct vm_area_struct來描述,一個region可能表示一個進程的堆供malloc()使用,也可能代表一個映射的文件如動態鏈接庫。region中page仍然需要分配,設置active/resident/page out狀態。
如果一個region代表一個映射文件,則它的vm_file字段將會被設置。通過遍歷vm_file->f_dentry->d_inode->i_mapping,該region相關的address_space將被找到。address_space擁有磁盤上基於page操作所需要的所有信息。
以下是這些structures的關係:
Process Address Space Descriptor
一個進程的地址空間由struct mm_struct描述,這也就是說每個進程中只有一個mm_struct其它在用戶線程中共享。
一個內核線程並不需要一個唯一的mm_struct,因爲內核新城永遠不會觸發用戶地址空間的缺頁異常或者訪問用戶地址空間。但是有一個例外,當page fault發生在vmalloc虛擬地址段內,缺頁異常的代碼認爲這是一個特殊的case,並使用master page table中的信息去更新當前進程的page table。由於內核線程並不需要mm_struct,所以內核線程的task_struct->mm字段總是NULL。
- lazy TLB
因爲刷新TLB緩存代價非常昂貴,所以一種lazy TLB的技術被使用來避免不會訪問用戶地址空間的進程進行不必要的TLB刷新。因爲內核地址空間的總是可見的。當調用switch_mm()會導致TLB刷新,但是它可以通過借用前一個task使用過的mm_struct,並把它放在task_struct->active_mm中來避免刷新TLB。這種技術極大提高了上下文切換的時間。
當要進入lazy TLB時,需要調用enter_lazy_tlb()來保證mm_struct不會再不同的處理器間共享。第二次需要使用lazy TLB的時候是當進程退出時等待其父進程的捕獲會調用start_lazy_tlb().
mm_struct中有兩種引用計數:mm_user和mm_count分別來表示兩種類型的使用者。
- mm_user是指正在訪問mm_struct中用戶地址空間的進程數,如:page table和文件映射。用戶空間的線程和swap_out()會增加mm_user的計數來保證mm_struct不會被提前銷燬,當mm_user變成0之後,exit_mmap()會刪除所有的映射和銷燬所有的page table,然後再講mm_count值減一。
- mm_count是指“匿名使用者”的引用計數。其初始化值爲1。一個匿名使用者是指該使用者並不關心mm_struct用戶地址空間的部分而僅僅是借用mm_struct.匿名使用者如使用lazy TLB的內核線程。當mm_count變成了0,該mm_struct纔可以被安全地銷燬。
使用兩種引用計數的原因是當用戶空間的映射全部被銷燬後,匿名使用者仍然需要mm_struct的情況。沒有一個位置可以延時銷燬page tables。
struct mm_struct的定義如下,下面來看看重點字段的含義:
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 task page tables and mm->rss */
struct list_head mmlist; /* List of all active 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, total_vm, locked_vm, shared_vm;
unsigned long exec_vm, stack_vm, reserved_vm, def_flags;
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;
};
- mmap:用戶地址空間中所有VMA的鏈表表頭
- mm_rb:VMA結構形成的紅黑樹的跟節點,用於快速查找。
- mmap_cache:調用find_vma()後找到的VMA會放置在該字段,其目的是假想該VMA會快被再次使用。
- pgd:當前進程的PGD
- mm_count,mm_user:詳細解釋見上小節。
- mmlist:mm_struct通過此字段連接在一起。
- start_code, end_code:當前進程的代碼段的起始地址和結束地址
- start_data, end_data:當前進程的數據段的起始地址和結束地址
- start_brk, brk:當前進程堆的起始地址和結束地址
- start_stack:當前進程棧的起始地址
- arg_start, arg_end:當前進程命令行參數的起始地址和結束地址
- env_start, env_end:當前進程環境變量的起始地址和結束地址
- rss:Resident Set Size是指當前進程中存在的page的個數,不包括global zero page。
- total_vm:當前進程所有VMA region所佔用的總內存空間大小
- locked_vm:resident page中被lock的個數
- swap_address:當一個進程被全部被換出時,pageout守護進程用來記錄上一個被換出的地址
Allocating a Descriptor
有兩個函數用來分配一個mm_struct.
- allocate_mm()該宏函數用來從slab allocator中分配一個mm_struct
- mm_alloc()從slab中分配mm_struct後再調用mm_init()將其初始化
Initialising a Descriptor
系統中最初始的mm_struct 叫init_mm,它使用宏INIT_MM()來靜態地初始化:
struct mm_struct init_mm = INIT_MM(init_mm);
#define INIT_MM(name) \
{ \
.mm_rb = RB_ROOT, \
.pgd = swapper_pg_dir, \
.mm_users = ATOMIC_INIT(2), \
.mm_count = ATOMIC_INIT(1), \
.mmap_sem = __RWSEM_INITIALIZER(name.mmap_sem), \
.page_table_lock = SPIN_LOCK_UNLOCKED, \
.mmlist = LIST_HEAD_INIT(name.mmlist), \
.cpu_vm_mask = CPU_MASK_ALL, \
.default_kioctx = INIT_KIOCTX(name.default_kioctx, name), \
}
當初始的mm_struct初始化完成後,新的mm_struct會以它們的父mm_struct爲模板而被創建。copy_mm()就是用來複制mm_struct,然後條用mm_init()來初始化進程特殊的字段。
Destroying a Descriptor
使用atomic_inc(&mm->mm_users)來增加mm_struct用戶空間的引用計數,相對應的使用mmput()來減少引用計數。如果mm_users的引用計數到達0,exit_mmap()會銷燬所有被映射的VMA regions和銷燬page tables因此此時已經沒有用戶空間的使用者了。
此時再來使用mmdrop()來講mm_count減一,因爲所有用戶空間的使用者被當成是mm_count中的一個計數。當mm_count變爲0後,便可銷燬mm_struct 了。
void mmput(struct mm_struct *mm)
{
if (atomic_dec_and_lock(&mm->mm_users, &mmlist_lock)) {
list_del(&mm->mmlist);
mmlist_nr--;
spin_unlock(&mmlist_lock);
exit_aio(mm);
exit_mmap(mm);
put_swap_token(mm);
mmdrop(mm);
}
}
Memory Regions
進程地址空間中全部的地址很少被使用,只有很少的regions被使用。Linux用struct vm_area_struct來描述一個VMA。下面來看看VMA的結構和重要字段的含義:
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 address within 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_SIZE units, *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_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
};
- vm_mm:當前VMA所屬的mm_struct
- vm_start,vm_end:當前VMA的起始和結束地址
- vm_next:地址空間中所有的VMA按照地址大小排序通過此字段單向連接起來
- vm_page_prot:VMA中所有PTE的保護標誌位
- vm_flags:一組標誌位用來保護和控制VMA的屬性
- vm_rb:所有VMA構成的紅黑樹,用於快速查找
- vm_next_share,vm_ppre_share:基於文件映射的共享VMA使用此字段連接在一起
- vm_ops:此字段包含的函數指針有:open(),close(),nopage()。它們用作與磁盤同步信息。
- vm_pgoff:當一個文件有內存映射時,該字段表示該region中的內容在整個文件中的偏移
- vm_file:此字段指向一個內存映射的struct file結構
Memory Region Operations
我們來看下vm_operations_struct的定義:
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 unused);
};
其中open()和close()每次在region被創建和被刪除時被調用。這兩個函數只有被很少的設備和一個文件系統以及SystemV使用。其中SystemV共享內存使用open()回調函數來增加VMA的個數。
其中最主要的回調函數是nopage()。當page fault產生是do_no_page()會調用nopage()這個回調函數,該函數的目的是從page cache中拿到page或者分配一個新的page然後返回地址。
大多數的內存映射的文件對應的vma會註冊一個vm_opetaionts_struct叫做generic_file_vm_ops,它註冊了一個nopage()回調:
struct vm_operations_struct generic_file_vm_ops = {
.nopage = filemap_nopage,
.populate = filemap_populate,
};
我們來看看share memory VMA的operation:
static struct vm_operations_struct shm_vm_ops = {
.open = shm_open, /* callback for a new vm-area open */
.close = shm_close, /* callback for when the vm-area is released */
.nopage = shmem_nopage,
#ifdef CONFIG_NUMA
.set_policy = shmem_set_policy,
.get_policy = shmem_get_policy,
#endif
};
File/Device backed memory regions
如果一個VMA對應了一個文件映射,那麼通過vm_file字段會找到一個對應的address_space的結構,該結構包含文件系統相關的信息如需要回寫到磁盤的髒頁。
首先來看下address_space的定義:
struct address_space {
struct inode *host; /* owner: inode, block_device */
struct radix_tree_root page_tree; /* radix tree of all pages */
spinlock_t tree_lock; /* and spinlock protecting it */
unsigned int i_mmap_writable;/* count VM_SHARED mappings */
struct prio_tree_root i_mmap; /* tree of private and shared mappings */
struct list_head i_mmap_nonlinear;/*list VM_NONLINEAR mappings */
spinlock_t i_mmap_lock; /* protect tree, count, list */
atomic_t truncate_count; /* Cover race condition with truncate */
unsigned long nrpages; /* number of total pages */
pgoff_t writeback_index;/* writeback starts here */
struct address_space_operations *a_ops; /* methods */
unsigned long flags; /* error bits/gfp mask */
struct backing_dev_info *backing_dev_info; /* device readahead, etc */
spinlock_t private_lock; /* for use by the address_space */
struct list_head private_list; /* ditto */
struct address_space *assoc_mapping; /* ditto */
};
struct address_space_operations {
int (*writepage)(struct page *page, struct writeback_control *wbc);
int (*readpage)(struct file *, struct page *);
int (*sync_page)(struct page *);
/* Write back some dirty pages from this mapping. */
int (*writepages)(struct address_space *, struct writeback_control *);
/* Set a page dirty */
int (*set_page_dirty)(struct page *page);
int (*readpages)(struct file *filp, struct address_space *mapping,
struct list_head *pages, unsigned nr_pages);
/*
* ext3 requires that a successful prepare_write() call be followed
* by a commit_write() call - they must be balanced
*/
int (*prepare_write)(struct file *, struct page *, unsigned, unsigned);
int (*commit_write)(struct file *, struct page *, unsigned, unsigned);
/* Unfortunately this kludge is needed for FIBMAP. Don't use it */
sector_t (*bmap)(struct address_space *, sector_t);
int (*invalidatepage) (struct page *, unsigned long);
int (*releasepage) (struct page *, int);
ssize_t (*direct_IO)(int, struct kiocb *, const struct iovec *iov,
loff_t offset, unsigned long nr_segs);
};
- writepage:寫一個page到磁盤,每個page所對應的文件偏移可以在struct page中找到。
- readpage:從磁盤中讀取一個page
- sync_page:將髒頁同步到磁盤
- prepare_write:將用戶空間的數據copy到要回寫磁盤的一個page之前,調用這個函數。
- commit_write:從用戶空間copy數據後,該函數用於提交信息到磁盤
- bmap:操作raw IO
- flushpage:保證在釋放一個page前沒有pending的IO
- releasepage:在釋放一個page前,嘗試刷新一個page對應的所有buff
- direct_IO:This function is used when performing direct IO to an inode
- direct_fileIO:Used to perform direct IO with a struct file
我們來看看共享內存文件系統的address_space operations:
static struct address_space_operations shmem_aops = {
.writepage = shmem_writepage,
.set_page_dirty = __set_page_dirty_nobuffers,
#ifdef CONFIG_TMPFS
.prepare_write = shmem_prepare_write,
.commit_write = simple_commit_write,
#endif
};
Page Faulting
進程線性地址空間中的頁不一定要駐留在內存中。例如,進程中的內存分配內核不會立即滿足分配對應的物理內存而是將線性地址使用vm_area_struct預留。還比如被交換到磁盤上的page。
Linux和其他大多數操作系統一樣,擁有按需獲取內存的策略,具體的做法是處理不再內存中的page。這就說明僅當硬件觸發缺頁異常操作系統捕獲異常後並分配頁,然互纔會從磁盤上讀取page到內存中。在Linux中,當一個page從交換區置換到主內存中,其後面的多個page頁會被同時讀入到swap_cache中。
當前主要有兩種類型的page fault:major fault 和minor fault。
- major fault是指當執行數據從不得不從磁盤中讀取的這種昂貴操作時發生的缺頁異常
- minor fault是指除了major fault的缺頁異常都是minor fault.
Linux中處理以下異常的方式:
Exception | Type | Action |
---|---|---|
vma region合法但page沒有分配 | Minor | 從物理地址分配器中分配一個頁框 |
vma region不合法但在可擴展region的邊上如stack | Minor | 擴展該region並且分配page |
page被換出但在swap緩存中 | Minor | 在進程頁表中重新創建page並且丟棄對swap緩存的引用 |
page被換出到磁盤介質上了 | Major | 使用PTE中的信息從磁盤上讀回page |
寫只讀頁 | Minor | 如果該page是一個COW頁,則copy一份並置爲可寫映射到進程當前地址空間,如果是非法寫,則發送SIG_SEGV |
region不合法或者進程沒有訪問權限 | Error | 發送SIG_SEGV |
缺頁異常發生在內核地址空間 | Minor | 如果發生缺頁異常的地址是在vmalloc地址空間,那麼當前進程的頁表將會被主內核頁表swapper_pg_dir中的內容更新,這是內核唯一合法的缺頁異常 |
缺頁異常發生在用戶地址空間但當前處於內核模式 | Error | 如果缺頁異常發生,這就意味着內核系統並沒有從用戶空間拷貝並且引發缺頁異常,這是內核的一個bug |
每種架構都會註冊自己的處理缺頁異常的函數。雖然這個函數的名稱是任意的,但通常的選擇是do_page_fault(),其調用草圖如下:
該函數中提供了豐富的信息,如發生缺頁異常的地址,是簡單的地找到不到page還是訪問權限的問題,或者是讀或者寫錯誤,再或者該地址是用戶空間還是內核空間。該函數的作用是要決定當前發生的是哪種錯誤並如何處理。其流程如下:
handle_mm_fault()函數來處理用戶空間的缺頁異常如:COW page,swapped out page等。其返回值的含義:
- 1,minor fault
- 2,major fault
- 0,錯誤
- other, 則會觸發out of memory處理函數
Handling a Page Fault
一旦缺頁處理函數決定當前缺頁異常是一個合法的缺頁異常,handle_mm_fault()將會被執行。
如下圖所示handle_mm_fault()中會根據PTE的屬性來選擇調用另外三個函數。首先第一步的決定是通過檢查PTE是否存在(pte_present())或者是否已分配(pte_none())。
- Demand Allocation:如果PTE沒有被分配及pte_none()返回True,do_no_page() & do_file_page()將被調用來處理Demand Allocation。
- Demend Paging:如果是一個被交換到磁盤中的page,則調用do_swap_page()處理Demend Paging
- COW page:如果是一個寫保護的page,且要發生寫的動作,則調用do_wp_page()來處理COW page.一個COW page是指被多個進程共享的page直到一個寫事件發生然後就copy這個page到寫進程的地址空間。一個COW page能夠被識別是因爲即使PTE有寫保護,VMA也會被標記爲可寫。如果不是一個COW page,因爲有寫入,該page將會標記爲dirty。
- 最後如果一個page被讀且存在但仍然發生錯誤,是因爲有些架構沒有三級頁表。在這種case中,就直接新建PTE,然後標記爲young。
Demand Allocation
當一個進程在最開始的時候來訪問一個page,系統將會通過do_no_page()函數來分配頁並將數據填充到page中。如果當前vma->vm_ops爲NULL,則是匿名page,如果不爲NULL,則是file/device backed page。下面分別來看這兩種情況:
Handling anonymous pages
當vm_area_struct->vm_ops字段爲空或者沒有提供nopage()函數,則調用do_anonymous_page()來處理這次匿名訪問。在這種case下只有兩種情況first read和first write
- first read:因爲是第一次讀所以是沒有數據的,因此只要把全局的empty_zero_page映射到該地址對應的PTE上,並且該PTE註上寫保護。因此當寫事件發生時,因爲寫保護所以會再次發生page fault。在x86中全局empty_zero_page 是在mem_init()中定義的。
- first write:如果是第一次寫,則會調用alloc_page()來分配一個空閒的page然後調用clear_user_highpage()將其填0。假設page被成功分配,則mm_struct中rss(Resident Set Size)字段將會加上一。在某些架構下當一個page插入到進程的用戶地址空間中時會調用flush_page_to_ram()來保證緩存的相干性。然後將該page插入到LRU list中一邊後續內存回收代碼可以回收這個page。
Handling file/device backed pages
如果一個地址有映射一個file或者device,vm_operation_struct中的vm_ops必須提供nopage()函數。nopage()函數負責分配一個page並從磁盤中讀出一個page的數據到該內存中。
當返回page之後,首先檢查page分配過程中是否出錯,再來檢查是否有early COW break發生。如果此次的page fault是以此寫操作並且VMA的flag中並未設置VM_SHARED,此時即表明一個early COW break發生了。early COW break是在減少該page的引用計數之前,分配一個新page並複製數據。(不太理解…)
然後再檢查該PTE是否存在,如果不存在生成PTE並映射到page table中。
Demand Paging
當一個page被交換到磁盤中時,do_swap_page()函數負責將該page再讀回來。通過PTE中的信息就可以找到該page在swap_cache中的位置。因爲一個page可能在多個進程中被共享,所以他們不可能立即被換出到磁盤上,而是把他們放置在swap cache中。
因爲有了swap cache的存在,因此當一個page fault發生的時候,該page可能存在於swap cache中。如果確實如此,則增加該page的引用計數然後把它放置到進程的page table中,然後註冊一個minor page fault。
如果該page在磁盤上,則調用read_swap_cache_async()將數據讀回,然後再將該page重新放置在進程的page table中。
Copy On Write (COW) Pages
當fork一個進程的時候,子進程會全部複製父進程的地址空間。但這是一種非常昂貴的操作,因此COW計數被用上了。
在fork的過程中,兩個進程的PTE全部標記爲只讀,因此當有一個寫動作發生時,提供會產生一個page fault。Linux之所以能識別COW page是因爲儘管PTE是寫保護但是其對應的VMA region是可以寫的。然後調用do_wp_page()函數來賦值一個page然後將其填充到寫進程的地址空間中。使用COW進行fork的時候,page table需要拷貝,而對應的數據不用拷貝。
Copying To/From Userspace
在進程地址空間中直接訪問物理內存時不安全的因爲沒有辦法快速檢查地址對應的page是否存在。當進程訪問不合法的地址時,Linux依賴MMU上報異常然後通過page fault處理函數來處理異常。在x86 case下,當遇到一個完全無法使用的地址時,有提供一個彙編函數__copy_usr()來追蹤異常。當調用search_exception_table()函數時就可以找到對應修復代碼的位置。Linux提供了一些宏函函數供內核態程序安全地從用戶空間拷貝數據或者拷貝數據到用戶空間,常見的有:
unsigned long copy_from_user(void *to, const void *from, unsigned long n);
unsigned long copy_to_user(void *to, const void *from, unsigned long n);
在編譯階段,鏈接器會在內核代碼段__ex_table中創建exception_table,__ex_table段的起始地址爲__start___ex_table,結束地址爲__stop___ex_table.。exception table中的每一項對應一個結構struct exception_table_entry
struct exception_table_entry {
unsigned long insn, fixup;
};
當遇到真正非法的地址,page fault handler會通過search_exception_table()來查找該地址是否有對應的修復代碼,如果有就執行修復代碼。
其核心步驟分爲三步:
- first:彙編函數負責從用戶空間拷貝實際需要size的數據,如果page不存在,page fault將會發生,如果地址合法,則page fault函數會自動處理好
- second:修復代碼
- third:通過_ex_table的映射關係,找到修復代碼,執行修復代碼