Linux內核剖析 之 進程地址空間(一)
緒論
內核獲取內存方式——直接了當:
1. 從分區頁框分配器獲取內存(__get_free_pages()或alloc_pages());
2. 使用slab分配器為專用或通用對象分配內存(kmem_cache_alloc()或kmalloc());
3. 使用vmalloc或vmalloc_32獲取一塊非連續內存區。
如果申請的內存得以滿足,這些函數返回一個頁描述符地址或線性地址。
*內核申請內存使用這些簡單方法基於以下兩個原因:
1.內核是操作係統中優先級最高的成分。如果內核申請動態內存,那麼必定有正當理由。因此,沒有理由推遲處理這個請求。
2.內核信任自己。所有內核函數都是假定沒有錯誤的,因此內核函數不必插入針對編程錯誤的任何保護措施。
*當給用戶態進程分配內存時,情況完全不同:
1.進程對動態內存的請求被認為是不緊迫的。內核總是盡量推遲給用戶態進程分配動態內存。
2.用戶進程是不可信任的,因此,內核必須能隨時準備捕捉用戶進程引起的所有尋址錯誤。
主要內容:
*內核如何實現對進程動態內存的推遲分配?
——使用一種新的資源(線性地址區)
當用戶進程請求動態內存時,並沒有獲得請求的頁框,而是獲得了一個新的線性地址的使用權。這線性地址區域就成為進程地址空間的一部分。
*為什麼要推遲分配?
*進程怎樣看待動態內存。
*進程地址空間的基本組成。
*缺頁異常處理程序在推遲給進程分配頁框中所起的作用。
*內核怎樣創建和刪除進程的整個地址空間?
*與進程的地址空間管理有關的API和係統調用。
====>>>>>
1. 進程地址空間
2. 內存描述符
3. 線性區
4. 缺頁異常處理程序
5. 創建和刪除進程的地址空間
6. 堆的管理
進程地址空間
進程的地址空間(Address Space)由允許進程使用的全部線性地址組成。一個進程使用的進程地址空間與另一個進程使用的進程地址空間之間沒有關係。如果進程之間共享相同的地址空間,則被稱為線程。
內核可以通過增加或刪除某些線性地址區間來動態修改進程的地址空間。
內核通過所謂線性區的資源來表示線性地址空間,線性地址空間由起始線性地址、長度以及相應訪問權限來描述。
起始地址和長度都是4096(一頁)的整數倍——提高效率。
*進程創建新的線性區的典型情況(六種):
程序執行
exec()函數
缺頁異常處理程序
內存映射
IPC共享內存
malloc()函數—heap
*與創建、刪除線性區相關的係統調用:
係統調用 |
說明 |
brk() |
改變進程堆的大小 |
execve() |
裝入可執行文件,從而改變進程的地址空間 |
_exit() |
結束當前進程並撤銷它的進程地址空間 |
fork() |
創建一個新的進程,並創建新的地址空間。 |
mmap(),mmap2() |
為文件創建一個內存映射,從而擴大進程的地址空間 |
mremap() |
擴大或縮小線性區 |
remap_file_pages() |
為文件創建非線性映射 |
munmap() |
撤銷對文件的內存映射,從而縮小進程的地址空間 |
shmat() |
創建一個共享線性區 |
shmdt() |
撤銷一個共享線性區 |
確定進程所擁有的線性地址區是內核的任務,這使得缺頁處理程序能夠有效的處理以下兩種無效的線性地址:
1. 由編程錯誤引發的非法地址訪問(如數組越界、非法指針);
2. 由缺頁(物理頁)引發的無效線性地址,即使這個線性地址屬於進程地址空間,但是內核還未分配物理頁。——請求調頁。
內存描述符
與進程地址空間的全部信息都包含在一個叫做內存描述符(Memory Descriptor)的數據結構中,這個結構的類型是mm_struct,進程描述符的mm字段指向此結構。
無論是內核線程還是用戶進程,對於內核來說,都是task_struct這個數據結構的一個實例,task_struct被稱為進程描述符(process descriptor),因為它記錄了這個進程所有的上下文(context)。其中有一個被稱為“內存描述符”(memory descriptor)的數據結構 mm_struct,該結構抽象並描述了Linux視角下管理進程地址空間的所有信息。
[start_code,end_code)表示代碼段的地址空間範圍。
[start_data,end_data)表示數據段的地址空間範圍。
[start_brk,brk)分別表示heap段的起始空間和當前的heap指針。
[start_stack,end_stack)表示stack段的地址空間範圍。
mmap_base表示memory mapping段的起始地址。
具體結構圖:
內存描述符:
mm_strcut定義:
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; unsigned long hiwater_rss; /* High-water RSS usage */ unsigned long hiwater_vm; /* High-water virtual memory usage */ };mm_struct字段:
類型 |
字段 |
說明 |
struct vm_area_struct* |
mmap |
指向線性區域對象的鏈表頭 |
struct rb_root |
mm_rb |
指向線性區對象的紅黑樹的根 |
struct vm_area_struct* |
mmap_cache |
指向最後一個引用的線性區對象 |
unsigned long (*) () |
get_unmapped_area |
在進程地址空間中搜索有效線性地址區 |
void (*) () |
unmap_area |
釋放線性地址區間時調用的方法 |
unsigned long |
free_area_cache |
內核從這個地址開始搜索進程地址空間中線性地址的空閑區域 |
pgd_t * |
pdg |
指向頁全局目錄 |
atomic_t |
mm_users |
次使用計數器 |
atomic_t |
mm_count |
主使用計數器 |
int |
map_count |
線性區的個數 |
struct rw_semaphore |
mmap_sem |
線性區的讀/寫信號量 |
spinlock_t |
page_table_lock |
線性區的自旋所和頁表的自旋鎖 |
struct list_head |
mmlist |
指向內存描述符鏈表中的相鄰元素 |
unsigned long |
start_code |
可執行代碼的起始地址 |
unsigned long |
end_code |
可執行代碼的最後地址 |
unsigned long |
start_data |
已初始化數據的起始地址 |
unsigned long |
end_data |
已初始化數據的最後地址 |
unsigned long |
start_brk |
堆的起始地址 |
unsigned long |
brk |
堆的當前最後地址 |
unsigned long |
start_stack |
用戶堆棧的起始地址 |
unsigned long |
arg_start |
命令行參數的起始地址 |
unsigned long |
arg_end |
命令行參數的最後地址 |
unsigned long |
env_start |
環境變量的起始地址 |
unsigned long |
env_end |
環境變量的最後地址 |
unsigned long |
rss |
分配給進程的頁框數 |
unsigned long |
anon_rss |
非配給匿名內存映射的頁框數 |
unsigned long |
total_vm |
進程地址空間的大小(頁數) |
unsigned long |
locked_vm |
鎖住而不能換出的頁的個數 |
unsigned long |
shared_vm |
共享文件內存映射中的頁數 |
unsigned long |
exec_vm |
可執行內存映射中的頁數 |
unsigned long |
stack_vm |
用戶堆棧中的頁數 |
unsigned long |
reserved_vm |
在保留區中的頁數或在特殊線性區中的頁數 |
unsigned long |
def_flags |
線性區默認的訪問標誌 |
unsigned long |
nr_ptes |
進程的頁表數 |
unsigned long [] |
saved_auxv |
開始執行ELF程序時使用 |
unsigned int |
dumpable |
表示是否可以產生內存轉儲信息的標誌 |
cpumask_t |
cpu_vm_mask |
用於惰性TLB交換的位掩碼 |
mm_context_t |
context |
指向有關特定體係結構信息的表 |
unsigned long |
swap_token_time |
進程在這個時間將有資格獲得交換標誌 |
char |
recent_pagein |
最近發生了主缺頁,則設置該標誌 |
int |
core_waiters |
正在把進程地址空間的內容轉儲到core文件中的輕量級進程的數目 |
struct completion * |
core_startup_done |
指向創建內存轉儲文件的補充原語 |
struct completion |
core_done |
指向創建內存轉儲文件的補充原語 |
rwlock_t |
ioctx_list_lock |
用於保護異步I/O上下文鏈表的鎖 |
struct kioctx * |
ioctx_list |
異步I/O上下文鏈表 |
struct kioctx |
default_kioctx |
默認的異步I/O上下文 |
unsigned long |
hiwater_rss |
進程所擁有的最大頁框數 |
unsigned long |
hiwater_vm |
進程線性區中的最大頁數 |
所有的內存描述符存放在一個雙向鏈表中。每個內存描述符在mmlist字段存放鏈表相鄰元素的地址。鏈表的第一個元素是init_mm的mmlist字段,init_mm是初始化階段進程0使用的內存描述符。
注意理解和比較兩個字段:mm_users和mm_count字段。
# mm_users字段存放共享mm_struct數據結構的輕量級進程的個數。
# mm_count字段是內存描述符的主使用計數器,在mm_users次使用計數器中的所有用戶在mm_count中隻作為一個單元。每當mm_count遞減時,內核都要檢查它是否變為0,如果是,就要解除這個內存描述符,因為不再有用戶使用它。
===>為什麼要設置mm_count字段?見下文。
內核線程的內存描述符
內核線程就能運行在內核態,因此,它們永遠不會訪問低於TASK_SIZE(等於PAGE_OFFSET)的地址。與普通進程相反,內核線程不用線性區。因此描述符的很多字段對內核線程是沒有意義的。
內核線程使用前一個進程的內存描述符。
在每個進程描述符中包含了兩種內存描述符指針:mm和active_mm。
進程描述符中的mm字段指向進程所擁有的內存描述符,而active_mm字段指向進程運行時所使用的內存描述符。對於普通進程,這兩個字段存放相同的指針。但是,對於內核線程,由於內核線程不擁有任何內存描述符,因此,它們的mm字段總是NULL。當內核線程運行時,它的active_mm字段被初始化為前一個運行進程的active_mm值。
這裏,可以解釋上麵mm_users字段和mm_count字段的區別:
<<<====>>>
mm_users字段記錄共享該內存描述符的普通進程的個數,mm_count的目的在於支持內核線程級別。
對Linux來說,用戶進程和內核線程(kernel thread)都是task_struct的實例(tsk),唯一的區別是內核線程是沒有進程地址空間的,內核線程也沒有mm內存描述符,所以內核線程的tsk->mm域(其中,struct task_struct * tsk)是空(NULL)。當內核scheduler在執行進程上下文切換(context switching)時,會根據tsk->mm判斷即將調度的進程是用戶進程還是內核線程。雖然內核線程不訪問用戶進程地址空間,但是仍然需要通過page table來訪問內核線程的內核地址空間。幸運的是,對於任何用戶進程來說,它們的內核空間都是完全相同的(3G-4G),所以內核可以“借用”上一個被調用的用戶進程的內存描述符mm中的頁表來訪問內核地址,並將此mm記錄在active_mm。簡而言之,對於用戶進程,tsk->mm == tsk->active_mm;而對於內核線程,tsk->mm == NULL表示自己內核線程的身份,而tsk->active_mm是使用上一個用戶進程的mm,通過此mm的page table來訪問內核空間。
為了支持內核線程級別,mm_struct裏麵引入了另外一個counter,主使用計數器mm_count。前麵有mm_users表示這個進程地址空間被多少普通進程共享或者引用,而mm_count則表示這個地址空間被內核線程引用的次數+1(這裏的1,指的是在mm_users次使用計數器中的所有用戶在mm_count中隻作為一個單元)。內核不會因為mm_users == 0而銷毀此mm_struct,內核隻會當mm_count == 0時才會釋放mm_struct,因為這個時候既沒有用戶進程使用這個地址空間,也沒有內核線程引用這個地址空間。
最後更新:2017-04-03 05:39:37