從用戶態(tài)、內(nèi)核態(tài)、全局變量、BSS函數(shù)看進程運行狀態(tài)
點擊上方“程序員大白”,選擇“星標(biāo)”公眾號
重磅干貨,第一時間送達
轉(zhuǎn)載:一口Linux
收集項目組需求的時候,我們知道一個進程要運行起來需要以下的內(nèi)存結(jié)構(gòu)。
用戶態(tài):
代碼段、全局變量、BSS
函數(shù)棧
堆
內(nèi)存映射區(qū)
內(nèi)核態(tài):
內(nèi)核的代碼、全局變量、BSS
內(nèi)核數(shù)據(jù)結(jié)構(gòu)例如 task_struct
內(nèi)核棧
內(nèi)核中動態(tài)分配的內(nèi)存
現(xiàn)在這些事是不是已經(jīng)都有了著落?
我畫了一個圖,總結(jié)一下進程運行狀態(tài)在 32 位下對應(yīng)關(guān)系。

對于 64 位的對應(yīng)關(guān)系,只是稍有區(qū)別,我這里也畫了一個圖,方便你對比理解。

用戶態(tài)和內(nèi)核態(tài)的劃分
進程的虛擬地址空間,其實就是站在項目組的角度來看內(nèi)存,所以我們就從 task_struct 出發(fā)來看。這里面有一個 struct mm_struct 結(jié)構(gòu)來管理內(nèi)存。
struct mm_struct *mm;在 struct mm_struct 里面,有這樣一個成員變量:
unsigned long task_size; /* size of task vm space */我們之前講過,整個虛擬內(nèi)存空間要一分為二,一部分是用戶態(tài)地址空間,一部分是內(nèi)核態(tài)地址空間,那這兩部分的分界線在哪里呢?這就要 task_size 來定義。
對于 32 未來的系統(tǒng),內(nèi)核里面是這樣定義 TASK_SIZE 答:
#ifdef CONFIG_X86_32
/*
* User space process size: 3GB (default).
*/
#define TASK_SIZE PAGE_OFFSET
#define TASK_SIZE_MAX TASK_SIZE
/*
config PAGE_OFFSET
hex
default 0xC0000000
depends on X86_32
*/
#else
/*
* User space process size. 47bits minus one guard page.
*/
#define TASK_SIZE_MAX ((1UL << 47) - PAGE_SIZE)
#define TASK_SIZE (test_thread_flag(TIF_ADDR32) ? \
IA32_PAGE_OFFSET : TASK_SIZE_MAX)
......當(dāng)執(zhí)行一個新的進程的時候,會做以下的設(shè)置:
current->mm->task_size = TASK_SIZE;
對于 32 位系統(tǒng),最大能夠?qū)ぶ?2^32=4G,其中用戶態(tài)虛擬地址空間是 3G,內(nèi)核態(tài)是 1G。
對于 64 位置系統(tǒng),虛擬地址只使用了 48 位。就像代碼里面寫的一樣,1 左移了 47 位,就相當(dāng)于 48 位地址空間一半的位置,0x0000800000000000,然后減去一個頁,就是 0x00007FFFFFFFF000,共 128T。同樣,內(nèi)核空間也是 128T。內(nèi)核空間和用戶空間之間隔著很大的空隙,以此來進行隔離。

用戶態(tài)布局
我們先來看用戶態(tài)虛擬空間的布局。
之前我們講了用戶態(tài)虛擬空間里面有幾類數(shù)據(jù),例如代碼、全局變量、堆、棧、內(nèi)存映射區(qū)等。在 struct mm_struct 里面,有下面這些變量定義了這些區(qū)域的統(tǒng)計信息和位置。
unsigned long mmap_base; /* base of mmap area */
unsigned long total_vm; /* Total pages mapped */
unsigned long locked_vm; /* Pages that have PG_mlocked set */
unsigned long pinned_vm; /* Refcount permanently increased */
unsigned long data_vm; /* VM_WRITE & ~VM_SHARED & ~VM_STACK */
unsigned long exec_vm; /* VM_EXEC & ~VM_WRITE & ~VM_STACK */
unsigned long stack_vm; /* VM_STACK */
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;其中,total_vm 是總共映射的頁的數(shù)目。我們知道,這么大的虛擬地址空間,不可能都有真實內(nèi)存對應(yīng),所以這里是映射的數(shù)目。當(dāng)內(nèi)存吃緊的時候,有些頁可以換出到硬盤上,有的頁因為比較重要,不能換出。locked_vm 就是被鎖定不能換出,pinned_vm 是不能換出,也不能移動。
data_vm 是存放數(shù)據(jù)的頁的數(shù)目,exec_vm 是存放可執(zhí)行文件的頁的數(shù)目,stack_vm 是棧所占的頁的數(shù)目。
start_code 和 end_code 表示可執(zhí)行代碼的開始和結(jié)束位置,start_data 和 end_data 表示已初始化數(shù)據(jù)的開始位置和結(jié)束位置。
start_brk 是堆的起始位置,brk 是堆當(dāng)前的結(jié)束位置。前面咱們講過 malloc 申請一小塊內(nèi)存的話,就是通過改變 brk 位置實現(xiàn)的。
start_stack 是棧的起始位置,棧的結(jié)束位置在寄存器的棧頂指針中。
arg_start 和 arg_end 是參數(shù)列表的位置, env_start 和 env_end 是環(huán)境變量的位置。它們都位于棧中最高地址的地方。
mmap_base 表示虛擬地址空間中用于內(nèi)存映射的起始地址。一般情況下,這個空間是從高地址到低地址增長的。前面咱們講 malloc 申請一大塊內(nèi)存的時候,就是通過 mmap 在這里映射一塊區(qū)域到物理內(nèi)存。咱們加載動態(tài)鏈接庫 so 文件,也是在這個區(qū)域里面,映射一塊區(qū)域到 so 文件。
這下所有用戶狀態(tài)的區(qū)域的位置基本上都描述清楚了。整個布局就像下面這張圖這樣。雖然 32 位和 64 位置的空間相差很大,但是區(qū)域的類別和布局是相似的。

除了位置信息之外,struct mm_struct 里面還專門有一個結(jié)構(gòu) vm_area_struct,來描述這些區(qū)域的屬性。
struct vm_area_struct *mmap; /* list of VMAs */
struct rb_root mm_rb;這里面一個是單鏈表,用于將這些區(qū)域串起來。另外還有一個紅黑樹。又是這個數(shù)據(jù)結(jié)構(gòu),在進程調(diào)度的時候我們用的也是紅黑樹。它的好處就是查找和修改都很快。這里用紅黑樹,就是為了快速查找一個內(nèi)存區(qū)域,并在需要改變的時候,能夠快速修改。
struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */
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, *vm_prev;
struct rb_node vm_rb;
struct mm_struct *vm_mm; /* The address space we belong to. */
struct list_head anon_vma_chain; /* Serialized by mmap_sem &
* page_table_lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;
struct file * vm_file; /* File we map to (can be NULL). */
void * vm_private_data; /* was vm_pte (shared mem) */
} __randomize_layout;vm_start 和 vm_end 指定了該區(qū)域在用戶空間中的起始和結(jié)束地址。vm_next 和 vm_prev 將這個區(qū)域串在鏈表上。vm_rb 將這個區(qū)域放在紅黑樹上。vm_ops 里面是對這個內(nèi)存區(qū)域可以做的操作的定義。
虛擬內(nèi)存區(qū)域可以映射到物理內(nèi)存,也可以映射到文件,映射到物理內(nèi)存的時候稱為匿名映射,anon_vma 中,anoy 就是 anonymous,匿名的意思,映射到文件就需要有 vm_file 指定被映射的文件。
那這些 vm_area_struct 是如何和上面的內(nèi)存區(qū)域關(guān)聯(lián)的呢?
這個事情是在 load_elf_binary 里面實現(xiàn)的。沒錯,就是它。加載內(nèi)核的是它,啟動第一個用戶態(tài)進程 init 的是它,fork 完了以后,調(diào)用 exec 運行一個二進制程序的也是它。
當(dāng) exec 運行一個二進制程序的時候,除了解析 ELF 的格式之外,另外一個重要的事情就是建立內(nèi)存映射。
static int load_elf_binary(struct linux_binprm *bprm)
{
......
setup_new_exec(bprm);
......
retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
executable_stack);
......
error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
elf_prot, elf_flags, total_size);
......
retval = set_brk(elf_bss, elf_brk, bss_prot);
......
elf_entry = load_elf_interp(&loc->interp_elf_ex,
interpreter,
&interp_map_addr,
load_bias, interp_elf_phdata);
......
current->mm->end_code = end_code;
current->mm->start_code = start_code;
current->mm->start_data = start_data;
current->mm->end_data = end_data;
current->mm->start_stack = bprm->p;
......
}load_elf_binary 會完成以下的事情:
調(diào)用 setup_new_exec,設(shè)置內(nèi)存映射區(qū) mmap_base;
調(diào)用 setup_arg_pages,設(shè)置棧的 vm_area_struct,這里面設(shè)置了 mm->arg_start 是指向棧底的,current->mm->start_stack 就是棧底;
elf_map 會將 ELF 文件中的代碼部分映射到內(nèi)存中來;
set_brk 設(shè)置了堆的 vm_area_struct,這里面設(shè)置了 current->mm->start_brk = current->mm->brk,也即堆里面還是空的;
load_elf_interp 將依賴的 so 映射到內(nèi)存中的內(nèi)存映射區(qū)域。
最終就形成下面這個內(nèi)存映射圖。
映射完畢后,什么情況下會修改呢?
第一種情況是函數(shù)的調(diào)用,涉及函數(shù)棧的改變,主要是改變棧頂指針。
第二種情況是通過 malloc 申請一個堆內(nèi)的空間,當(dāng)然底層要么執(zhí)行 brk,要么執(zhí)行 mmap。關(guān)于內(nèi)存映射的部分,我們后面的章節(jié)講,這里我們重點看一下 brk 是怎么做的。
brk 系統(tǒng)調(diào)用實現(xiàn)的入口是 sys_brk 函數(shù),就像下面代碼定義的一樣。
SYSCALL_DEFINE1(brk, unsigned long, brk)
{
unsigned long retval;
unsigned long newbrk, oldbrk;
struct mm_struct *mm = current->mm;
struct vm_area_struct *next;
......
newbrk = PAGE_ALIGN(brk);
oldbrk = PAGE_ALIGN(mm->brk);
if (oldbrk == newbrk)
goto set_brk;
/* Always allow shrinking brk. */
if (brk <= mm->brk) {
if (!do_munmap(mm, newbrk, oldbrk-newbrk, &uf))
goto set_brk;
goto out;
}
/* Check against existing mmap mappings. */
next = find_vma(mm, oldbrk);
if (next && newbrk + PAGE_SIZE > vm_start_gap(next))
goto out;
/* Ok, looks good - let it rip. */
if (do_brk(oldbrk, newbrk-oldbrk, &uf) < 0)
goto out;
set_brk:
mm->brk = brk;
......
return brk;
out:
retval = mm->brk;
return retval前面我們講過了,堆是從低地址向高地址增長的,sys_brk 函數(shù)的參數(shù) brk 是新的堆頂位置,而當(dāng)前的 mm->brk 是原來堆頂?shù)奈恢谩?/span>
首先要做的第一個事情,將原來的堆頂和現(xiàn)在的堆頂,都按照頁對齊地址,然后比較大小。如果兩者相同,說明這次增加的堆的量很小,還在一個頁里面,不需要另行分配頁,直接跳到 set_brk 那里,設(shè)置 mm->brk 為新的 brk 就可以了。
如果發(fā)現(xiàn)新舊堆頂不在一個頁里面,麻煩了,這下要跨頁了。如果發(fā)現(xiàn)新堆頂小于舊堆頂,這說明不是新分配內(nèi)存了,而是釋放內(nèi)存了,釋放的還不小,至少釋放了一頁,于是調(diào)用 do_munmap 將這一頁的內(nèi)存映射去掉。
如果堆將要擴大,就要調(diào)用 find_vma。如果打開這個函數(shù),看到的是對紅黑樹的查找,找到的是原堆頂所在的 vm_area_struct 的下一個 vm_area_struct,看當(dāng)前的堆頂和下一個 vm_area_struct 之間還能不能分配一個完整的頁。如果不能,沒辦法只好直接退出返回,內(nèi)存空間都被占滿了。
如果還有空間,就調(diào)用 do_brk 進一步分配堆空間,從舊堆頂開始,分配計算出的新舊堆頂之間的頁數(shù)。
static int do_brk(unsigned long addr, unsigned long len, struct list_head *uf)
{
return do_brk_flags(addr, len, 0, uf);
}
static int do_brk_flags(unsigned long addr, unsigned long request, unsigned long flags, struct list_head *uf)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma, *prev;
unsigned long len;
struct rb_node **rb_link, *rb_parent;
pgoff_t pgoff = addr >> PAGE_SHIFT;
int error;
len = PAGE_ALIGN(request);
......
find_vma_links(mm, addr, addr + len, &prev, &rb_link,
&rb_parent);
......
vma = vma_merge(mm, prev, addr, addr + len, flags,
NULL, NULL, pgoff, NULL, NULL_VM_UFFD_CTX);
if (vma)
goto out;
......
vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
INIT_LIST_HEAD(&vma->anon_vma_chain);
vma->vm_mm = mm;
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_pgoff = pgoff;
vma->vm_flags = flags;
vma->vm_page_prot = vm_get_page_prot(flags);
vma_link(mm, vma, prev, rb_link, rb_parent);
out:
perf_event_mmap(vma);
mm->total_vm += len >> PAGE_SHIFT;
mm->data_vm += len >> PAGE_SHIFT;
if (flags & VM_LOCKED)
mm->locked_vm += (len >> PAGE_SHIFT);
vma->vm_flags |= VM_SOFTDIRTY;
return 0;在 do_brk 中,調(diào)用 find_vma_links 找到將來的 vm_area_struct 節(jié)點在紅黑樹的位置,找到它的父節(jié)點、前序節(jié)點。接下來調(diào)用 vma_merge,看這個新節(jié)點是否能夠和現(xiàn)有樹中的節(jié)點合并。如果地址是連著的,能夠合并,則不用創(chuàng)建新的 vm_area_struct 了,直接跳到 out,更新統(tǒng)計值即可;如果不能合并,則創(chuàng)建新的 vm_area_struct,既加到 anon_vma_chain 鏈表中,也加到紅黑樹中。
內(nèi)核態(tài)的布局
用戶態(tài)虛擬空間分析完畢,接下來我們分析內(nèi)核態(tài)虛擬空間。
內(nèi)核態(tài)的虛擬空間和某一個進程沒有關(guān)系,所有進程通過系統(tǒng)調(diào)用進入到內(nèi)核之后,看到的虛擬地址空間都是一樣的。
這里強調(diào)一下,千萬別以為到了內(nèi)核里面,咱們就會直接使用物理內(nèi)存地址了,想當(dāng)然地認(rèn)為下面討論的都是物理內(nèi)存地址,不是的,這里討論的還是虛擬內(nèi)存地址,但是由于內(nèi)核總是涉及管理物理內(nèi)存,因而總是隱隱約約發(fā)生關(guān)系,所以這里必須思路清晰,分清楚物理內(nèi)存地址和虛擬內(nèi)存地址。
在內(nèi)核態(tài),32 位和 64 位的布局差別比較大,主要是因為 32 位內(nèi)核態(tài)空間太小了。
我們來看 32 位的內(nèi)核態(tài)的布局。

32 位的內(nèi)核態(tài)虛擬地址空間一共就 1G,占絕大部分的前 896M,我們稱為直接映射區(qū)。
所謂的直接映射區(qū),就是這一塊空間是連續(xù)的,和物理內(nèi)存是非常簡單的映射關(guān)系,其實就是虛擬內(nèi)存地址減去 3G,就得到物理內(nèi)存的位置。
在內(nèi)核里面,有兩個宏:
__pa(vaddr) 返回與虛擬地址 vaddr 相關(guān)的物理地址;
__va(paddr) 則計算出對應(yīng)于物理地址 paddr 的虛擬地址。
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
#define __pa(x) __phys_addr((unsigned long)(x))
#define __phys_addr(x) __phys_addr_nodebug(x)
#define __phys_addr_nodebug(x) ((x) - PAGE_OFFSET)但是你要注意,這里虛擬地址和物理地址發(fā)生了關(guān)聯(lián)關(guān)系,在物理內(nèi)存的開始的 896M 的空間,會被直接映射到 3G 至 3G+896M 的虛擬地址,這樣容易給你一種感覺,是這些內(nèi)存訪問起來和物理內(nèi)存差不多,別這樣想,在大部分情況下,對于這一段內(nèi)存的訪問,在內(nèi)核中,還是會使用虛擬地址的,并且將來也會為這一段空間建設(shè)頁表,對這段地址的訪問也會走上一節(jié)我們講的分頁地址的流程,只不過頁表里面比較簡單,是直接的一一對應(yīng)而已。
這 896M 還需要仔細分解。在系統(tǒng)啟動的時候,物理內(nèi)存的前 1M 已經(jīng)被占用了,從 1M 開始加載內(nèi)核代碼段,然后就是內(nèi)核的全局變量、BSS 等,也是 ELF 里面涵蓋的。這樣內(nèi)核的代碼段,全局變量,BSS 也就會被映射到 3G 后的虛擬地址空間里面。具體的物理內(nèi)存布局可以查看 /proc/iomem。
在內(nèi)核運行的過程中,如果碰到系統(tǒng)調(diào)用創(chuàng)建進程,會創(chuàng)建 task_struct 這樣的實例,內(nèi)核的進程管理代碼會將實例創(chuàng)建在 3G 至 3G+896M 的虛擬空間中,當(dāng)然也會被放在物理內(nèi)存里面的前 896M 里面,相應(yīng)的頁表也會被創(chuàng)建。
在內(nèi)核運行的過程中,會涉及內(nèi)核棧的分配,內(nèi)核的進程管理的代碼會將內(nèi)核棧創(chuàng)建在 3G 至 3G+896M 的虛擬空間中,當(dāng)然也就會被放在物理內(nèi)存里面的前 896M 里面,相應(yīng)的頁表也會被創(chuàng)建。
896M 這個值在內(nèi)核中被定義為 high_memory,在此之上常稱為“高端內(nèi)存”。這是個很籠統(tǒng)的說法,到底是虛擬內(nèi)存的 3G+896M 以上的是高端內(nèi)存,還是物理內(nèi)存 896M 以上的是高端內(nèi)存呢?
這里仍然需要辨析一下,高端內(nèi)存是物理內(nèi)存的概念。它僅僅是內(nèi)核中的內(nèi)存管理模塊看待物理內(nèi)存的時候的概念。前面我們也說過,在內(nèi)核中,除了內(nèi)存管理模塊直接操作物理地址之外,內(nèi)核的其他模塊,仍然要操作虛擬地址,而虛擬地址是需要內(nèi)存管理模塊分配和映射好的。
假設(shè)咱們的電腦有 2G 內(nèi)存,現(xiàn)在如果內(nèi)核的其他模塊想要訪問物理內(nèi)存 1.5G 的地方,應(yīng)該怎么辦呢?如果你覺得,我有 32 位的總線,訪問個 2G 還不小菜一碟,這就錯了。
首先,你不能使用物理地址。你需要使用內(nèi)存管理模塊給你分配的虛擬地址,但是虛擬地址的 0 到 3G 已經(jīng)被用戶態(tài)進程占用去了,你作為內(nèi)核不能使用。因為你寫 1.5G 的虛擬內(nèi)存位置,一方面你不知道應(yīng)該根據(jù)哪個進程的頁表進行映射;另一方面,就算映射了也不是你真正想訪問的物理內(nèi)存的地方,所以你發(fā)現(xiàn)你作為內(nèi)核,能夠使用的虛擬內(nèi)存地址,只剩下 1G 減去 896M 的空間了。
于是,我們可以將剩下的虛擬內(nèi)存地址分成下面這幾個部分。
在 896M 到 VMALLOC_START 之間有 8M 的空間。
VMALLOC_START 到 VMALLOC_END 之間稱為內(nèi)核動態(tài)映射空間,也即內(nèi)核想像用戶態(tài)進程一樣 malloc 申請內(nèi)存,在內(nèi)核里面可以使用 vmalloc。假設(shè)物理內(nèi)存里面,896M 到 1.5G 之間已經(jīng)被用戶態(tài)進程占用了,并且映射關(guān)系放在了進程的頁表中,內(nèi)核 vmalloc 的時候,只能從分配物理內(nèi)存 1.5G 開始,就需要使用這一段的虛擬地址進行映射,映射關(guān)系放在專門給內(nèi)核自己用的頁表里面。
PKMAP_BASE 到 FIXADDR_START 的空間稱為持久內(nèi)核映射。使用 alloc_pages() 函數(shù)的時候,在物理內(nèi)存的高端內(nèi)存得到 struct page 結(jié)構(gòu),可以調(diào)用 kmap 將其在映射到這個區(qū)域。
FIXADDR_START 到 FIXADDR_TOP(0xFFFF F000) 的空間,稱為固定映射區(qū)域,主要用于滿足特殊需求。
在最后一個區(qū)域可以通過 kmap_atomic 實現(xiàn)臨時內(nèi)核映射。假設(shè)用戶態(tài)的進程要映射一個文件到內(nèi)存中,先要映射用戶態(tài)進程空間的一段虛擬地址到物理內(nèi)存,然后將文件內(nèi)容寫入這個物理內(nèi)存供用戶態(tài)進程訪問。給用戶態(tài)進程分配物理內(nèi)存頁可以通過 alloc_pages(),分配完畢后,按說將用戶態(tài)進程虛擬地址和物理內(nèi)存的映射關(guān)系放在用戶態(tài)進程的頁表中,就完事大吉了。這個時候,用戶態(tài)進程可以通過用戶態(tài)的虛擬地址,也即 0 至 3G 的部分,經(jīng)過頁表映射后訪問物理內(nèi)存,并不需要內(nèi)核態(tài)的虛擬地址里面也劃出一塊來,映射到這個物理內(nèi)存頁。但是如果要把文件內(nèi)容寫入物理內(nèi)存,這件事情要內(nèi)核來干了,這就只好通過 kmap_atomic 做一個臨時映射,寫入物理內(nèi)存完畢后,再 kunmap_atomic 來解映射即可。
32 位的內(nèi)核態(tài)布局我們看完了,接下來我們再來看 64 位的內(nèi)核布局。
其實 64 位的內(nèi)核布局反而簡單,因為虛擬空間實在是太大了,根本不需要所謂的高端內(nèi)存,因為內(nèi)核是 128T,根本不可能有物理內(nèi)存超過這個值。
64 位的內(nèi)存布局如圖所示。

64 位的內(nèi)核主要包含以下幾個部分。
從 0xffff800000000000 開始就是內(nèi)核的部分,只不過一開始有 8T 的空檔區(qū)域。
從 __PAGE_OFFSET_BASE(0xffff880000000000) 開始的 64T 的虛擬地址空間是直接映射區(qū)域,也就是減去 PAGE_OFFSET 就是物理地址。虛擬地址和物理地址之間的映射在大部分情況下還是會通過建立頁表的方式進行映射。
從 VMALLOC_START(0xffffc90000000000)開始到 VMALLOC_END(0xffffe90000000000)的 32T 的空間是給 vmalloc 的。
從 VMEMMAP_START(0xffffea0000000000)開始的 1T 空間用于存放物理頁面的描述結(jié)構(gòu) struct page 的。
從 __START_KERNEL_map(0xffffffff80000000)開始的 512M 用于存放內(nèi)核代碼段、全局變量、BSS 等。這里對應(yīng)到物理內(nèi)存開始的位置,減去 __START_KERNEL_map 就能得到物理內(nèi)存的地址。這里和直接映射區(qū)有點像,但是不矛盾,因為直接映射區(qū)之前有 8T 的空當(dāng)區(qū)域,早就過了內(nèi)核代碼在物理內(nèi)存中加載的位置。
到這里內(nèi)核中虛擬空間的布局就介紹完了。
推薦閱讀
關(guān)于程序員大白
程序員大白是一群哈工大,東北大學(xué),西湖大學(xué)和上海交通大學(xué)的碩士博士運營維護的號,大家樂于分享高質(zhì)量文章,喜歡總結(jié)知識,歡迎關(guān)注[程序員大白],大家一起學(xué)習(xí)進步!


