在x86體系結構中分段機制是必選的,而分頁機制則可由具體的作業系統而選擇,Linux通過讓段的基位址為0而巧妙的繞過了基位址。是以,對于Linux來說,虛位址和線性位址是一緻的。在32位的平台上,線性位址的大小為固定的4GB。并且,由于采用了保護機制,Linux核心将這4GB分為兩部分,虛位址較高的1GB(0xC0000000到0xFFFFFFFF)為共享的核心空間;而較低的3GB(0x00000000到0xBFFFFFFF)為每個程序的使用者空間。由于每個程序都不能直接通路核心空間,而是通過系統調用間接進入核心,是以,所有的程序都共享核心空間。而每個程序都擁有各自的使用者空間,各個程序之間不能互相通路彼此的使用者空間。是以,對于每一個具體的程序而言,都擁有4GB的虛拟位址空間。
一個程式在經過編譯、連接配接之後形成的位址空間是一個虛拟的位址空間,隻有當程式運作的時候才會配置設定具體的實體空間。由此我們可以得知,程式的虛拟位址相對來說是固定的,而實體位址則随着每一次程式的運作而有所不同。
對于核心空間而言,它與實體記憶體之間存在一個簡單的線性關系,即存在3GB的偏移量。在Linux核心中,這個偏移量叫做PAGE_OFFSET。如果核心的某個實體位址為x,那麼對應的核心虛位址就為x+PAGE_OFFSET。
對于使用者空間而言,它與實體記憶體之間的映射遠不止這麼簡單。與核心空間和實體空間的線性映射不同的是,分頁機制将虛拟使用者空間和實體位址空間分成大小相同的頁,然後再通過頁表将虛拟頁和實體頁塊映射起來。
核心空間一般可以通過__get_free_page()、kmalloc()和vmalloc()來申請核心空間。隻不過__get_free_page函數每次申請的都是完整的頁;而後兩者則依據具體參數申請以位元組為機關的記憶體空間。此外,前兩個函數申請的虛拟位址空間和實體位址空間都是連續的;vmalloc函數申請的實體位址空間并不連續。vmalloc函數通過重建立立虛拟位址空間和實體位址空間之間的映射,即建立頁表項,将離散的實體位址空間映射到連續的虛拟位址空間。是以,使用該函數的開銷比較大。
下面的程式簡單的示範了這三個函數的使用方法。從結果中可以看出,這些函數申請的位址都在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的空間是如何劃分的?通常,除了我們熟悉的代碼段和資料段,使用者空間還包括堆棧段和堆。我們可以通過下面的示範程式來了解這些區域到底負責存儲程式的那些内容。 int 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 可以看到,代碼段存放程式的代碼;資料段存放全局變量和static類型的局部變量。此外,未初始化的全局變量雖然也存在于資料段,但是這些未初始化的變量都集中在靠近資料段上邊界的區域,這個區域稱為BSS段。以上這些空間是程序所必須擁有的,它們在程序運作之前就配置設定好了。 程式中的局部變量一般被配置設定在堆棧段,其位于使用者空間最頂部。與固定的代碼段和資料段不同的是,堆棧段存儲資料是從高低值往低位址延伸的。是以,在資料段到堆棧段之間,形成了一片空洞,這片空洞用于存儲malloc函數所動态配置設定的空間,這片空洞區域被稱為堆。 通過下面這個圖可以更進一步的了解到程序使用者空間的劃分情況。
以上是關于程序使用者空間劃分的大緻分析,上述理論在核心代碼中如何展現?它将涉及到mm_struct結構和vm_area_struct結構。 每一個程序都擁有3GB大小的使用者空間,而連續使用者空間又按照存儲内容的不同被劃分成若幹個區域。在核心中,主要通過mm_struct結構體和vm_area_struct結構體對程序使用者空間進行描述。前者是對程序的使用者空間進行整體的描述;而後者則是對使用者空間中的某個區域進行描述。顯然,每一個程序對應的有一個mm_struct結構和多個vm_area_struct結構。 1.mm_struct結構 最新版本中的mm_struct結構字段比較多,接下來隻對部分字段做以說明。 mmap:vm_area_struct結構體類型的指針。指向程序使用者空間中各區域所組成的雙連結清單。連結清單方式可以高效的周遊所有元素; mm_rb:rb_root結構體類型。同樣描述記憶體區域塊,隻不過采用紅黑樹來表示。用紅黑樹可以快速索引到指定的元素; mm_users:atomic_t類型。用來記錄正在使用該位址空間的程序數目。比如,目前有3個程序正在共享該位址空間,那麼其值為3; mm_count:atomic_t類型。記錄mm_struct結構體被引用的次數。如果目前該位址空間隻被兩個程序所共享,那麼該值為1,mm_users為2;當這兩個程序都退出時,該值為0,mm_users也為0。另外,核心線程并不需要通路使用者的記憶體空間,也并不需要建立頁表。核心線程一般會直接使用前一個程序的mm_struct結構。是以該字段的計數還包括核心線程對這個結構的引用。 map_count:int類型。記憶體區域的個數; pgd:pgd_t類型,該結構體類型内部封裝的是unsigned
long類型的資料。pgd表示的是頁目錄基址。當排程程式排程一個程序運作時,就将這個線性位址轉化為實體位址,并寫入CR3控制寄存器中; 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類型。指令行參數所在記憶體的起始位址和終止位址,環境變量所在記憶體的起始位址和終止位址; 2.vm_area_struct結構 上面我們已經知道,該結構體描述的是程序使用者空間中的一個虛拟記憶體區間(Virtual Memory Area,VMA)。 vm_mm:mm_struct結構體類型指針。指向該區域所屬的使用者空間對應的mm_struct結構體。 vm_start,vm_end:unsigned long類型。該虛存區域的起始位址和終止位址。 vm_next,vm_prev:vm_area_struct結構體類型指針。構成VMA雙聯表。 vm_flags:unsigned
long類型。該虛存區的标志。 vm_page_prot:pgprot_t結構體類型,内部封裝了unsigned long類型。通路控制權限。 vm_ops:vm_operations_struct結構體類型。該虛存區域的操作函數接口,這些函數可以對虛存區中的頁進行操作。 3.資料結構的關系 了解了上述結構體的關鍵字段,它們與程序之間的邏輯關系便是我們接下來要關心的重點。我們知道,一個程序在核心中使用task_struct結構對其進行描述。task_struct結構中有一個mm字段,它所指向的便是與該程序使用者空間所對應的mm_struct結構體。通過上述分析,我們知道mm_struct結構中有mmap字段,它指向VMA雙連結清單。是以,我們使用current->mm->mmap就可以獲得VMA連結清單的頭指針。那麼current->mm->mmap->vm->next就可以獲得指向該VMA雙聯表的下一個結點的指針。 4.動手檢視記憶體區域 上述我們從代碼角度分析了使用者位址空間和記憶體區域。那麼對于一個任意的程序,我們如何檢視它的記憶體空間和所劃分的記憶體區域? 我們先看一個簡單的測試程式: int
main(void) { int i=1; char *str=NULL; str=(char *)malloc(sizeof(char)*1119); sleep(1000); return 0; } 這個程式中使用到了malloc函數,是以str變量存儲于堆中。我們通過列印/proc/3530/maps檔案,即可看到該程序的記憶體空間劃分。其中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] 每一行資訊依次顯示的内容為記憶體區域其實位址-終止位址,通路權限,偏移量,主裝置号:次裝置号,inode,檔案。 上面的資訊不但包含了test可執行對象的各記憶體區域,而且還分别顯示了
/lib/ld-2.11.1.so(動态連接配接程式)檔案和/lib/tls/i686/cmov/libc-2.11.1.so(C庫)檔案的記憶體區域資訊。 從某個記憶體區域的通路權限上可以大緻判斷該區域的類型。各個屬性符号的意義為:r-read,w-write,x-execute,s-shared,p-private。是以,r-x一般代表程式的代碼段,即可讀,可執行。rw-可能代表資料段,BSS段和堆棧段等,即可讀,可寫。堆棧段從行資訊的檔案名就可以區分;如果某行資訊的檔案名為空,那麼可能是BSS段。另外,上述test程序共享了核心動态庫,是以在00441000-00442000行處檔案名顯示為vdso(Virtual
Dynamic Shared Object)。