在x86體系結構中分段機制是必選的,而分頁機制則可由具體的操作系統(tǒng)而選擇,Linux通過讓段的基地址為0而巧妙的繞過了基地址。因此,對于Linux來說,虛地址和線性地址是一致的。在32位的平臺上,線性地址的大小為固定的4GB。并且,由于采用了保護機制,Linux內(nèi)核將這4GB分為兩部分,虛地址較高的1GB(0xC0000000到0xFFFFFFFF)為共享的內(nèi)核空間;而較低的3GB(0x00000000到0xBFFFFFFF)為每個進程的用戶空間。由于每個進程都不能直接訪問內(nèi)核空間,而是通過系統(tǒng)調(diào)用間接進入內(nèi)核,因此,所有的進程都共享內(nèi)核空間。而每個進程都擁有各自的用戶空間,各個進程之間不能互相訪問彼此的用戶空間。因此,對于每一個具體的進程而言,都擁有4GB的虛擬地址空間?! ?div> 一個程序在經(jīng)過編譯、連接之后形成的地址空間是一個虛擬的地址空間,只有當程序運行的時候才會分配具體的物理空間。由此我們可以得知,程序的虛擬地址相對來說是固定的,而物理地址則隨著每一次程序的運行而有所不同。 對于內(nèi)核空間而言,它與物理內(nèi)存之間存在一個簡單的線性關系,即存在3GB的偏移量。在Linux內(nèi)核中,這個偏移量叫做PAGE_OFFSET。如果內(nèi)核的某個物理地址為x,那么對應的內(nèi)核虛地址就為x+PAGE_OFFSET。 對于用戶空間而言,它與物理內(nèi)存之間的映射遠不止這么簡單。與內(nèi)核空間和物理空間的線性映射不同的是,分頁機制將虛擬用戶空間和物理地址空間分成大小相同的頁,然后再通過頁表將虛擬頁和物理頁塊映射起來。 內(nèi)核空間一般可以通過__get_free_page()、kmalloc()和vmalloc()來申請內(nèi)核空間。只不過__get_free_page函數(shù)每次申請的都是完整的頁;而后兩者則依據(jù)具體參數(shù)申請以字節(jié)為單位的內(nèi)存空間。此外,前兩個函數(shù)申請的虛擬地址空間和物理地址空間都是連續(xù)的;vmalloc函數(shù)申請的物理地址空間并不連續(xù)。vmalloc函數(shù)通過重新建立虛擬地址空間和物理地址空間之間的映射,即新建頁表項,將離散的物理地址空間映射到連續(xù)的虛擬地址空間。因此,使用該函數(shù)的開銷比較大。 下面的程序簡單的演示了這三個函數(shù)的使用方法。從結果中可以看出,這些函數(shù)申請的地址都在3GB(0xBFFFFFFF)以上。完整代碼在如下。
static int __init menroyshow_init(void) { printk("mmshow module is
working\n"); pagemem = __get_free_page(GFP_KERNEL); if(!pagemem) goto
gfp_fail; printk(KERN_INFO "pagemem = 0x%lx\n",pagemem); kmallocmem =
kmalloc(100 * sizeof(char),GFP_KERNEL); if(!kmallocmem) goto kmalloc_fail; printk(KERN_INFO "kmallocmem = 0x%p\n",kmallocmem); vmallocmem =
vmalloc(1000000 * sizeof(char)); if(!vmallocmem) goto vmalloc_fail; printk(KERN_INFO "vmallocmem = 0x%p\n",vmallocmem); return 0; gfp_fail: free_page(pagemem); kmalloc_fail: kfree(kmallocmem); vmalloc_fail: vfree(vmallocmem); return -1; } //運行結果: # pagemem = 0xf3211000 #
kmallocmem = 0xd581e700 # vmallocmem = 0xf9251000 每個進程夠擁有屬于自己的3GB的虛擬空間(用戶空間),那么這個3GB的空間是如何劃分的?通常,除了我們熟悉的代碼段和數(shù)據(jù)段,用戶空間還包括堆棧段和堆。我們可以通過下面的演示程序來了解這些區(qū)域到底負責存儲程序的那些內(nèi)容?! nt
bss_var; int data_var0 = 1; int main(int argc,char **argv) { printf("The
user space's address division of a process as follow:\n"); printf("Data
segment:\n"); printf("address of \"main\" function:%p\n\n",main);
printf("Data segment:\n"); printf("address of
data_var:%p\n",&data_var0); static int data_var1 = 4; printf("new end of
data_var:%p\n\n",&data_var1); printf("BSS:\n"); printf("address of
bss_var:%p\n\n",&bss_var); char *str = (char *)malloc(sizeof(char)*10);
printf("initial heap end:%p\n",str); char *buf = (char
*)malloc(sizeof(char)*10); printf("new heap end:%p\n\n",buf); int stack_var0
= 2; printf("Stack segment:\n"); printf("initial end of
stack:%p\n",&stack_var0); int stack_var1 = 3; printf("new end of
stack:%p\n",&stack_var1); return 0; } //運行結果: The user space's
address division of a process as follow: Data segment: address of "main"
function:0x8048454 Data segment: address of data_var:0x804a01c new end of
data_var:0x804a020 BSS: address of bss_var:0x804a02c initial heap
end:0x8f77008 new heap end:0x8f77018 Stack segment: initial end of
stack:0xbfe0a3b4 new end of stack:0xbfe0a3b0
可以看到,代碼段存放程序的代碼;數(shù)據(jù)段存放全局變量和static類型的局部變量。此外,未初始化的全局變量雖然也存在于數(shù)據(jù)段,但是這些未初始化的變量都集中在靠近數(shù)據(jù)段上邊界的區(qū)域,這個區(qū)域稱為BSS段。以上這些空間是進程所必須擁有的,它們在進程運行之前就分配好了?! 〕绦蛑械木植孔兞恳话惚环峙湓诙褩6?,其位于用戶空間最頂部。與固定的代碼段和數(shù)據(jù)段不同的是,堆棧段存儲數(shù)據(jù)是從高低值往低地址延伸的。因此,在數(shù)據(jù)段到堆棧段之間,形成了一片空洞,這片空洞用于存儲malloc函數(shù)所動態(tài)分配的空間,這片空洞區(qū)域被稱為堆?! ⊥ㄟ^下面這個圖可以更進一步的了解到進程用戶空間的劃分情況?! ?
以上是關于進程用戶空間劃分的大致分析,上述理論在內(nèi)核代碼中如何體現(xiàn)?它將涉及到mm_struct結構和vm_area_struct結構?! ∶恳粋€進程都擁有3GB大小的用戶空間,而連續(xù)用戶空間又按照存儲內(nèi)容的不同被劃分成若干個區(qū)域。在內(nèi)核中,主要通過mm_struct結構體和vm_area_struct結構體對進程用戶空間進行描述。前者是對進程的用戶空間進行整體的描述;而后者則是對用戶空間中的某個區(qū)域進行描述。顯然,每一個進程對應的有一個mm_struct結構和多個vm_area_struct結構?! ?.mm_struct結構 最新版本中的mm_struct結構字段比較多,接下來只對部分字段做以說明?! map:vm_area_struct結構體類型的指針。指向進程用戶空間中各區(qū)域所組成的雙鏈表。鏈表方式可以高效的遍歷所有元素; mm_rb:rb_root結構體類型。同樣描述內(nèi)存區(qū)域塊,只不過采用紅黑樹來表示。用紅黑樹可以快速索引到指定的元素; mm_users:atomic_t類型。用來記錄正在使用該地址空間的進程數(shù)目。比如,當前有3個進程正在共享該地址空間,那么其值為3; mm_count:atomic_t類型。記錄mm_struct結構體被引用的次數(shù)。如果當前該地址空間只被兩個進程所共享,那么該值為1,mm_users為2;當這兩個進程都退出時,該值為0,mm_users也為0。另外,內(nèi)核線程并不需要訪問用戶的內(nèi)存空間,也并不需要創(chuàng)建頁表。內(nèi)核線程一般會直接使用前一個進程的mm_struct結構。因此該字段的計數(shù)還包括內(nèi)核線程對這個結構的引用?! ap_count:int類型。內(nèi)存區(qū)域的個數(shù); pgd:pgd_t類型,該結構體類型內(nèi)部封裝的是unsigned
long類型的數(shù)據(jù)。pgd表示的是頁目錄基址。當調(diào)度程序調(diào)度一個進程運行時,就將這個線性地址轉(zhuǎn)化為物理地址,并寫入CR3控制寄存器中; start_code,
end_code, start_data, end_data:unsigned long類型。進程代碼段和數(shù)據(jù)段的起始地址和終止地址; start_brk,
brk, start_stack:unsigned
long類型。分別為堆的起始地址和終止地址,堆棧的起始地址。上文說過,進程的堆棧段是根據(jù)需求向下(朝低地址方向)延伸的,因此這里并沒有堆棧段的終止地址; arg_start,
arg_end, env_start, env_end:unsigned
long類型。命令行參數(shù)所在內(nèi)存的起始地址和終止地址,環(huán)境變量所在內(nèi)存的起始地址和終止地址; 2.vm_area_struct結構 上面我們已經(jīng)知道,該結構體描述的是進程用戶空間中的一個虛擬內(nèi)存區(qū)間(Virtual
Memory
Area,VMA)?! m_mm:mm_struct結構體類型指針。指向該區(qū)域所屬的用戶空間對應的mm_struct結構體?! m_start,vm_end:unsigned
long類型。該虛存區(qū)域的起始地址和終止地址?! m_next,vm_prev:vm_area_struct結構體類型指針。構成VMA雙聯(lián)表。 vm_flags:unsigned
long類型。該虛存區(qū)的標志?! m_page_prot:pgprot_t結構體類型,內(nèi)部封裝了unsigned
long類型。訪問控制權限?! m_ops:vm_operations_struct結構體類型。該虛存區(qū)域的操作函數(shù)接口,這些函數(shù)可以對虛存區(qū)中的頁進行操作。 3.數(shù)據(jù)結構的關系 了解了上述結構體的關鍵字段,它們與進程之間的邏輯關系便是我們接下來要關心的重點。我們知道,一個進程在內(nèi)核中使用task_struct結構對其進行描述。task_struct結構中有一個mm字段,它所指向的便是與該進程用戶空間所對應的mm_struct結構體。通過上述分析,我們知道m(xù)m_struct結構中有mmap字段,它指向VMA雙鏈表。因此,我們使用current->mm->mmap就可以獲得VMA鏈表的頭指針。那么current->mm->mmap->vm->next就可以獲得指向該VMA雙聯(lián)表的下一個結點的指針?! ?.動手查看內(nèi)存區(qū)域 上述我們從代碼角度分析了用戶地址空間和內(nèi)存區(qū)域。那么對于一個任意的進程,我們?nèi)绾尾榭此膬?nèi)存空間和所劃分的內(nèi)存區(qū)域? 我們先看一個簡單的測試程序: int
main(void) { int i=1; char *str=NULL; str=(char
*)malloc(sizeof(char)*1119); sleep(1000); return 0;
} 這個程序中使用到了malloc函數(shù),因此str變量存儲于堆中。我們通過打印/proc/3530/maps文件,即可看到該進程的內(nèi)存空間劃分。其中3530是該進程的id?! ?
cat /proc/3530/maps 0014a000-00165000 r-xp 00000000 08:07 398276
/lib/ld-2.11.1.so 00165000-00166000 r--p 0001a000 08:07 398276
/lib/ld-2.11.1.so 00166000-00167000 rw-p 0001b000 08:07 398276
/lib/ld-2.11.1.so 001d8000-0032b000 r-xp 00000000 08:07 421931
/lib/tls/i686/cmov/libc-2.11.1.so 0032b000-0032c000 ---p 00153000 08:07 421931
/lib/tls/i686/cmov/libc-2.11.1.so 0032c000-0032e000 r--p 00153000 08:07 421931
/lib/tls/i686/cmov/libc-2.11.1.so 0032e000-0032f000 rw-p 00155000 08:07 421931
/lib/tls/i686/cmov/libc-2.11.1.so 0032f000-00332000 rw-p 00000000 00:00 0
00441000-00442000 r-xp 00000000 00:00 0 [vdso] 08048000-08049000 r-xp
00000000 08:09 326401 /home/edsionte/test 08049000-0804a000 r--p 00000000
08:09 326401 /home/edsionte/test 0804a000-0804b000 rw-p 00001000 08:09 326401
/home/edsionte/test 08958000-08979000 rw-p 00000000 00:00 0 [heap]
b78ce000-b78cf000 rw-p 00000000 00:00 0 b78dd000-b78e0000 rw-p 00000000
00:00 0 bfa6a000-bfa7f000 rw-p 00000000 00:00 0 [stack]
每一行信息依次顯示的內(nèi)容為內(nèi)存區(qū)域其實地址-終止地址,訪問權限,偏移量,主設備號:次設備號,inode,文件?! ∩厦娴男畔⒉坏藅est可執(zhí)行對象的各內(nèi)存區(qū)域,而且還分別顯示了
/lib/ld-2.11.1.so(動態(tài)連接程序)文件和/lib/tls/i686/cmov/libc-2.11.1.so(C庫)文件的內(nèi)存區(qū)域信息。 從某個內(nèi)存區(qū)域的訪問權限上可以大致判斷該區(qū)域的類型。各個屬性符號的意義為:r-read,w-write,x-execute,s-shared,p-private。因此,r-x一般代表程序的代碼段,即可讀,可執(zhí)行。rw-可能代表數(shù)據(jù)段,BSS段和堆棧段等,即可讀,可寫。堆棧段從行信息的文件名就可以區(qū)分;如果某行信息的文件名為空,那么可能是BSS段。另外,上述test進程共享了內(nèi)核動態(tài)庫,所以在00441000-00442000行處文件名顯示為vdso(Virtual
Dynamic Shared Object)。
|
|