偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

Linux 內(nèi)核的頁(yè)面錯(cuò)誤:原因與解決方案

系統(tǒng) Linux
虛擬內(nèi)存與 MMU 的這種映射機(jī)制,為 Linux 系統(tǒng)的內(nèi)存管理奠定了堅(jiān)實(shí)的基礎(chǔ),同時(shí)也為頁(yè)異常的發(fā)生埋下了伏筆,當(dāng)進(jìn)程訪問(wèn)的虛擬地址在頁(yè)表中找不到對(duì)應(yīng)的物理地址映射時(shí),頁(yè)異常就會(huì)登場(chǎng) 。

當(dāng)程序訪問(wèn)虛擬內(nèi)存中的一個(gè)頁(yè)面時(shí),如果該頁(yè)面當(dāng)前不在物理內(nèi)存中,就會(huì)觸發(fā)一個(gè)稱為"page fault"(頁(yè)異常)的異常。操作系統(tǒng)需要處理這個(gè)異常,并將所需頁(yè)面從磁盤加載到內(nèi)存中。實(shí)現(xiàn)虛存管理的一個(gè)關(guān)鍵是page fault異常處理,其過(guò)程中主要涉及到函數(shù) — do_pgfault的具體實(shí)現(xiàn)。

比如,在程序的執(zhí)行過(guò)程中由于某種原因(頁(yè)框不存在/寫(xiě)只讀頁(yè)等)而使 CPU 無(wú)法最終訪問(wèn)到相應(yīng)的物理內(nèi)存單元,即無(wú)法完成從虛擬地址到物理地址映射時(shí),CPU 會(huì)產(chǎn)生一次頁(yè)訪問(wèn)異常,從而需要進(jìn)行相應(yīng)的頁(yè)訪問(wèn)異常的中斷服務(wù)例程。這個(gè)頁(yè)訪問(wèn)異常處理的時(shí)機(jī)被操作系統(tǒng)充分利用來(lái)完成虛存管理,即實(shí)現(xiàn)“按需調(diào)頁(yè)”/“頁(yè)換入換出”處理的執(zhí)行時(shí)機(jī)。當(dāng)相關(guān)處理完成后,頁(yè)訪問(wèn)異常服務(wù)例程會(huì)返回到產(chǎn)生異常的指令處重新執(zhí)行,使得應(yīng)用軟件可以繼續(xù)正常運(yùn)行下去。

一、內(nèi)存管理前奏:虛擬內(nèi)存與MMU

在深入探討頁(yè)異常之前,先來(lái)了解一下 Linux 內(nèi)存管理的基礎(chǔ)架構(gòu)。在 Linux 系統(tǒng)中,進(jìn)程并不直接訪問(wèn)物理內(nèi)存,而是通過(guò)內(nèi)存管理單元(Memory Management Unit,MMU)來(lái)管理虛擬地址與物理地址的映射關(guān)系。

想象一下,你正在玩一款角色扮演游戲,每個(gè)角色都有自己獨(dú)立的背包(虛擬地址空間),背包里的物品位置(虛擬地址)與實(shí)際倉(cāng)庫(kù)(物理內(nèi)存)中的存儲(chǔ)位置是通過(guò)游戲管理員(MMU)來(lái)協(xié)調(diào)的。這樣,每個(gè)角色都覺(jué)得自己的背包很大,有足夠的空間放置物品,而實(shí)際上倉(cāng)庫(kù)的空間是有限的。這就是虛擬內(nèi)存的作用,它讓進(jìn)程以為自己擁有很大的連續(xù)內(nèi)存空間,而不必關(guān)心物理內(nèi)存的實(shí)際布局和大小限制。

什么是虛擬內(nèi)存?

簡(jiǎn)單地說(shuō)是指程序員或CPU“看到”的內(nèi)存。但有幾點(diǎn)需要注意:

  • 虛擬內(nèi)存單元不一定有實(shí)際的物理內(nèi)存單元對(duì)應(yīng),即實(shí)際的物理內(nèi)存單元可能不存在;
  • 如果虛擬內(nèi)存單元對(duì)應(yīng)有實(shí)際的物理內(nèi)存單元,那二者的地址一般是不相等的;
  • 通過(guò)操作系統(tǒng)實(shí)現(xiàn)的某種內(nèi)存映射可建立虛擬內(nèi)存與物理內(nèi)存的對(duì)應(yīng)關(guān)系,使得程序員或CPU訪問(wèn)的虛擬內(nèi)存地址會(huì)自動(dòng)轉(zhuǎn)換為一個(gè)物理內(nèi)存地址。

那么這個(gè)“虛擬”的作用或意義在哪里體現(xiàn)呢?在操作系統(tǒng)中,虛擬內(nèi)存其實(shí)包含多個(gè)虛擬層次,在不同的層次體現(xiàn)了不同的作用。首先,在有了分頁(yè)機(jī)制后,程序員或CPU“看到”的地址已經(jīng)不是實(shí)際的物理地址了,這已經(jīng)有一層虛擬化,我們可簡(jiǎn)稱為內(nèi)存地址虛擬化。有了內(nèi)存地址虛擬化,我們就可以通過(guò)設(shè)置頁(yè)表項(xiàng)來(lái)限定軟件運(yùn)行時(shí)的訪問(wèn)空間,確保軟件運(yùn)行不越界,完成內(nèi)存訪問(wèn)保護(hù)的功能。

虛擬內(nèi)存地址空間的引入,不僅解決了物理內(nèi)存不足的問(wèn)題,還提供了內(nèi)存保護(hù)和進(jìn)程隔離的功能。每個(gè)進(jìn)程都有自己獨(dú)立的虛擬地址空間,彼此之間互不干擾,就像不同的游戲角色在各自的背包里操作物品,不會(huì)影響到其他角色的背包。這樣,一個(gè)進(jìn)程的內(nèi)存訪問(wèn)錯(cuò)誤不會(huì)導(dǎo)致整個(gè)系統(tǒng)崩潰,大大提高了系統(tǒng)的穩(wěn)定性和安全性。

通過(guò) MMU 的映射,虛擬地址被轉(zhuǎn)換為物理地址,這個(gè)過(guò)程就像是游戲管理員根據(jù)角色背包里的物品位置信息,到實(shí)際倉(cāng)庫(kù)中找到對(duì)應(yīng)的物品。MMU 通過(guò)維護(hù)頁(yè)表(Page Table)來(lái)記錄虛擬地址與物理地址的映射關(guān)系,頁(yè)表就像是一本詳細(xì)的地址轉(zhuǎn)換字典,MMU 根據(jù)虛擬地址在頁(yè)表中查找對(duì)應(yīng)的物理地址。

在 32 位的 Linux 系統(tǒng)中,虛擬地址空間通常為 4GB,其中一部分用于用戶空間,另一部分用于內(nèi)核空間。用戶空間的進(jìn)程只能訪問(wèn)自己的虛擬地址空間,無(wú)法直接訪問(wèn)內(nèi)核空間,這種隔離機(jī)制有效地保護(hù)了內(nèi)核的安全,防止用戶進(jìn)程的非法操作對(duì)內(nèi)核造成破壞 。例如,普通用戶在自己的權(quán)限范圍內(nèi)進(jìn)行文件操作,無(wú)法直接訪問(wèn)系統(tǒng)核心文件,保障了系統(tǒng)的穩(wěn)定性。

虛擬內(nèi)存與 MMU 的這種映射機(jī)制,為 Linux 系統(tǒng)的內(nèi)存管理奠定了堅(jiān)實(shí)的基礎(chǔ),同時(shí)也為頁(yè)異常的發(fā)生埋下了伏筆,當(dāng)進(jìn)程訪問(wèn)的虛擬地址在頁(yè)表中找不到對(duì)應(yīng)的物理地址映射時(shí),頁(yè)異常就會(huì)登場(chǎng) 。

二、數(shù)據(jù)結(jié)構(gòu)與函數(shù)

首先是初始化過(guò)程。參考ucore總控函數(shù)init的代碼,可以看到在調(diào)用完成虛擬內(nèi)存初始化的vmm_init函數(shù)之前,需要首先調(diào)用pmm_init函數(shù)完成物理內(nèi)存的管理,這也是我們lab2已經(jīng)完成的內(nèi)容。接著是執(zhí)行中斷和異常相關(guān)的初始化工作,即調(diào)用pic_init函數(shù)和idt_init函數(shù)等,這些工作與lab1的中斷異常初始化工作的內(nèi)容是相同的。

在調(diào)用完idt_init函數(shù)之后,將進(jìn)一步調(diào)用三個(gè)lab3中才有的新函數(shù)vmm_init、ide_init和swap_init。這三個(gè)函數(shù)涉及了本次實(shí)驗(yàn)中的兩個(gè)練習(xí)。第一個(gè)函數(shù)vmm_init是檢查我們的練習(xí)1是否正確實(shí)現(xiàn)了。為了表述不在物理內(nèi)存中的“合法”虛擬頁(yè),需要有數(shù)據(jù)結(jié)構(gòu)來(lái)描述這樣的頁(yè),為此ucore建立了mm_struct和vma_struct數(shù)據(jù)結(jié)構(gòu)(接下來(lái)的小節(jié)中有進(jìn)一步詳細(xì)描述),假定我們已經(jīng)描述好了這樣的“合法”虛擬頁(yè),當(dāng)ucore訪問(wèn)這些“合法”虛擬頁(yè)時(shí),會(huì)由于沒(méi)有虛實(shí)地址映射而產(chǎn)生頁(yè)訪問(wèn)異常。如果我們正確實(shí)現(xiàn)了練習(xí)1,則do_pgfault函數(shù)會(huì)申請(qǐng)一個(gè)空閑物理頁(yè),并建立好虛實(shí)映射關(guān)系,從而使得這樣的“合法”虛擬頁(yè)有實(shí)際的物理頁(yè)幀對(duì)應(yīng)。這樣練習(xí)1就算完成了。

ide_init和swap_init是為練習(xí)2準(zhǔn)備的。由于頁(yè)面置換算法的實(shí)現(xiàn)存在對(duì)硬盤數(shù)據(jù)塊的讀寫(xiě),所以ide_init就是完成對(duì)用于頁(yè)換入換出的硬盤(簡(jiǎn)稱swap硬盤)的初始化工作。完成ide_init函數(shù)后,ucore就可以對(duì)這個(gè)swap硬盤進(jìn)行讀寫(xiě)操作了。swap_init函數(shù)首先建立swap_manager,swap_manager是完成頁(yè)面替換過(guò)程的主要功能模塊,其中包含了頁(yè)面置換算法的實(shí)現(xiàn)(具體內(nèi)容可參考5小節(jié))。

然后會(huì)進(jìn)一步調(diào)用執(zhí)行check_swap函數(shù)在內(nèi)核中分配一些頁(yè),模擬對(duì)這些頁(yè)的訪問(wèn),這會(huì)產(chǎn)生頁(yè)訪問(wèn)異常。如果我們正確實(shí)現(xiàn)了練習(xí)2,就可通過(guò)do_pgfault來(lái)調(diào)用swap_map_swappable函數(shù)來(lái)查詢這些頁(yè)的訪問(wèn)情況并間接調(diào)用實(shí)現(xiàn)頁(yè)面置換算法的相關(guān)函數(shù),把“不常用”的頁(yè)換出到磁盤上。

ucore在實(shí)現(xiàn)上述技術(shù)時(shí),需要解決三個(gè)關(guān)鍵問(wèn)題:

  • 當(dāng)程序運(yùn)行中訪問(wèn)內(nèi)存產(chǎn)生pagefault異常時(shí),如何判定這個(gè)引起異常的虛擬地址內(nèi)存訪問(wèn)是越界、寫(xiě)只讀頁(yè)的“非法地址”訪問(wèn)還是由于數(shù)據(jù)被臨時(shí)換出到磁盤上或還沒(méi)有分配內(nèi)存的“合法地址”訪問(wèn)?
  • 何時(shí)進(jìn)行請(qǐng)求調(diào)頁(yè)/頁(yè)換入換出處理?
  • 如何在現(xiàn)有ucore的基礎(chǔ)上實(shí)現(xiàn)頁(yè)替換算法?

接下來(lái)將進(jìn)一步分析完成lab3主要注意的關(guān)鍵問(wèn)題和涉及的關(guān)鍵數(shù)據(jù)結(jié)構(gòu)。

對(duì)于第一個(gè)問(wèn)題的出現(xiàn),在于實(shí)驗(yàn)二中有關(guān)內(nèi)存的數(shù)據(jù)結(jié)構(gòu)和相關(guān)操作都是直接針對(duì)實(shí)際存在的資源—物理內(nèi)存空間的管理,沒(méi)有從一般應(yīng)用程序?qū)?nèi)存的“需求”考慮,即需要有相關(guān)的數(shù)據(jù)結(jié)構(gòu)和操作來(lái)體現(xiàn)一般應(yīng)用程序?qū)μ摂M內(nèi)存的“需求”。一般應(yīng)用程序的對(duì)虛擬內(nèi)存的“需求”與物理內(nèi)存空間的“供給”沒(méi)有直接的對(duì)應(yīng)關(guān)系,ucore是通過(guò)page fault異常處理來(lái)間接完成這二者之間的銜接。

page_fault函數(shù)不知道哪些是“合法”的虛擬頁(yè),原因是ucore還缺少一定的數(shù)據(jù)結(jié)構(gòu)來(lái)描述這種不在物理內(nèi)存中的“合法”虛擬頁(yè)。為此ucore通過(guò)建立mm_struct和vma_struct數(shù)據(jù)結(jié)構(gòu),描述了ucore模擬應(yīng)用程序運(yùn)行所需的合法內(nèi)存空間。當(dāng)訪問(wèn)內(nèi)存產(chǎn)生page fault異常時(shí),可獲得訪問(wèn)的內(nèi)存的方式(讀或?qū)懀┮约熬唧w的虛擬內(nèi)存地址,這樣ucore就可以查詢此地址,看是否屬于vma_struct數(shù)據(jù)結(jié)構(gòu)中描述的合法地址范圍中,如果在,則可根據(jù)具體情況進(jìn)行請(qǐng)求調(diào)頁(yè)/頁(yè)換入換出處理(這就是練習(xí)2涉及的部分);如果不在,則報(bào)錯(cuò)。mm_struct和vma_struct數(shù)據(jù)結(jié)構(gòu)結(jié)合頁(yè)表表示虛擬地址空間和物理地址空間的示意圖如下所示:

在ucore中描述應(yīng)用程序?qū)μ摂M內(nèi)存“需求”的數(shù)據(jù)結(jié)構(gòu)是vma_struct(定義在vmm.h中),以及針對(duì)vma_struct的函數(shù)操作。這里把一個(gè)vma_struct結(jié)構(gòu)的變量簡(jiǎn)稱為vma變量。vma_struct的定義如下:

struct vma_struct {
    // the set of vma using the same PDT
    struct mm_struct *vm_mm;
    uintptr_t vm_start; // start addr of vma
    uintptr_t vm_end; // end addr of vma
    uint32_t vm_flags; // flags of vma
    //linear list link which sorted by start addr of vma
    list_entry_t list_link;
};

vm_start和vm_end描述了一個(gè)連續(xù)地址的虛擬內(nèi)存空間的起始位置和結(jié)束位置,這兩個(gè)值都應(yīng)該是PGSIZE 對(duì)齊的,而且描述的是一個(gè)合理的地址空間范圍(即嚴(yán)格確保 vm_start < vm_end的關(guān)系);list_link是一個(gè)雙向鏈表,按照從小到大的順序把一系列用vma_struct表示的虛擬內(nèi)存空間鏈接起來(lái),并且還要求這些鏈起來(lái)的vma_struct應(yīng)該是不相交的,即vma之間的地址空間無(wú)交集;vm_flags表示了這個(gè)虛擬內(nèi)存空間的屬性,目前的屬性包括:

#define VM_READ 0x00000001 //只讀
#define VM_WRITE 0x00000002 //可讀寫(xiě)
#define VM_EXEC 0x00000004 //可執(zhí)行

vm_mm是一個(gè)指針,指向一個(gè)比vma_struct更高的抽象層次的數(shù)據(jù)結(jié)構(gòu)mm_struct,這里把一個(gè)mm_struct結(jié)構(gòu)的變量簡(jiǎn)稱為mm變量。這個(gè)數(shù)據(jù)結(jié)構(gòu)表示了包含所有虛擬內(nèi)存空間的共同屬性,具體定義如下:

struct mm_struct {
    // linear list link which sorted by start addr of vma
    list_entry_t mmap_list;
    // current accessed vma, used for speed purpose
    struct vma_struct *mmap_cache;
    pde_t *pgdir; // the PDT of these vma
    int map_count; // the count of these vma
    void *sm_priv; // the private data for swap manager
};

mmap_list是雙向鏈表頭,鏈接了所有屬于同一頁(yè)目錄表的虛擬內(nèi)存空間,mmap_cache是指向當(dāng)前正在使用的虛擬內(nèi)存空間,由于操作系統(tǒng)執(zhí)行的“局部性”原理,當(dāng)前正在用到的虛擬內(nèi)存空間在接下來(lái)的操作中可能還會(huì)用到,這時(shí)就不需要查鏈表,而是直接使用此指針就可找到下一次要用到的虛擬內(nèi)存空間。由于mmap_cache 的引入,可使得 mm_struct 數(shù)據(jù)結(jié)構(gòu)的查詢加速 30% 以上。

pgdir所指向的就是 mm_struct數(shù)據(jù)結(jié)構(gòu)所維護(hù)的頁(yè)表。通過(guò)訪問(wèn)pgdir可以查找某虛擬地址對(duì)應(yīng)的頁(yè)表項(xiàng)是否存在以及頁(yè)表項(xiàng)的屬性等。map_count記錄mmap_list 里面鏈接的 vma_struct的個(gè)數(shù)。sm_priv指向用來(lái)鏈接記錄頁(yè)訪問(wèn)情況的鏈表頭,這建立了mm_struct和后續(xù)要講到的swap_manager之間的聯(lián)系。

涉及vma_struct的操作函數(shù)也比較簡(jiǎn)單,主要包括三個(gè):

  • vma_create—?jiǎng)?chuàng)建vma
  • insert_vma_struct—插入一個(gè)vma
  • find_vma—查詢vma。

vma_create函數(shù)根據(jù)輸入?yún)?shù)vm_start、vm_end、vm_flags來(lái)創(chuàng)建并初始化描述一個(gè)虛擬內(nèi)存空間的vma_struct結(jié)構(gòu)變量。insert_vma_struct函數(shù)完成把一個(gè)vma變量按照其空間位置[vma->vm_start,vma->vm_end]從小到大的順序插入到所屬的mm變量中的mmap_list雙向鏈表中。find_vma根據(jù)輸入?yún)?shù)addr和mm變量,查找在mm變量中的mmap_list雙向鏈表中某個(gè)vma包含此addr,即vma->vm_start<=addr end。這三個(gè)函數(shù)與后續(xù)講到的page fault異常處理有緊密聯(lián)系。

涉及mm_struct的操作函數(shù)比較簡(jiǎn)單,只有mm_create和mm_destroy兩個(gè)函數(shù),從字面意思就可以看出是是完成mm_struct結(jié)構(gòu)的變量創(chuàng)建和刪除。在mm_create中用kmalloc分配了一塊空間,所以在mm_destroy中也要對(duì)應(yīng)進(jìn)行釋放。在ucore運(yùn)行過(guò)程中,會(huì)產(chǎn)生描述虛擬內(nèi)存空間的vma_struct結(jié)構(gòu),所以在mm_destroy中也要進(jìn)對(duì)這些mmap_list中的vma進(jìn)行釋放。

三、Page Fault異常處理

當(dāng)進(jìn)程訪問(wèn)它的虛擬地址空間中的 PAGE 時(shí),如果這個(gè) PAGE 目前還不在物理內(nèi)存中,此時(shí) CPU 就像一個(gè)找不到文件的辦事員,無(wú)法繼續(xù)工作。Linux 會(huì)立即產(chǎn)生一個(gè) hard page fault 中斷,這就像是辦事員向上級(jí)報(bào)告文件缺失的情況 。

在這個(gè)過(guò)程中,系統(tǒng)需要從慢速設(shè)備(如磁盤)將對(duì)應(yīng)的數(shù)據(jù) PAGE 讀入物理內(nèi)存,就好比從倉(cāng)庫(kù)(磁盤)中找到文件并取出來(lái)。然后,建立物理內(nèi)存地址與虛擬地址空間 PAGE 的映射關(guān)系,這一步就像是給文件貼上標(biāo)簽,標(biāo)明它在虛擬地址空間中的位置。只有完成這些步驟后,進(jìn)程才能訪問(wèn)這部分虛擬地址空間的內(nèi)存,辦事員才能繼續(xù)處理文件。

產(chǎn)生頁(yè)訪問(wèn)異常的原因主要有:

  • 目標(biāo)頁(yè)幀不存在(頁(yè)表項(xiàng)全為0,即該線性地址與物理地址尚未建立映射或者已經(jīng)撤銷);
  • 相應(yīng)的物理頁(yè)幀不在內(nèi)存中(頁(yè)表項(xiàng)非空,但Present標(biāo)志位=0,比如在swap分區(qū)或磁盤文件上),這在本次實(shí)驗(yàn)中會(huì)出現(xiàn),我們將在下面介紹換頁(yè)機(jī)制實(shí)現(xiàn)時(shí)進(jìn)一步講解如何處理;
  • 不滿足訪問(wèn)權(quán)限(此時(shí)頁(yè)表項(xiàng)P標(biāo)志=1,但低權(quán)限的程序試圖訪問(wèn)高權(quán)限的地址空間,或者有程序試圖寫(xiě)只讀頁(yè)面)

當(dāng)出現(xiàn)上面情況之一,那么就會(huì)產(chǎn)生頁(yè)面page fault(#PF)異常。CPU會(huì)把產(chǎn)生異常的線性地址存儲(chǔ)在CR2中,并且把表示頁(yè)訪問(wèn)異常類型的值(簡(jiǎn)稱頁(yè)訪問(wèn)異常錯(cuò)誤碼,errorCode)保存在中斷棧中。

頁(yè)訪問(wèn)異常錯(cuò)誤碼有32位。位0為1表示對(duì)應(yīng)物理頁(yè)不存在;位1為1表示寫(xiě)異常(比如寫(xiě)了只讀頁(yè);位2為1表示訪問(wèn)權(quán)限異常(比如用戶態(tài)程序訪問(wèn)內(nèi)核空間的數(shù)據(jù))CR2是頁(yè)故障線性地址寄存器,保存最后一次出現(xiàn)頁(yè)故障的全32位線性地址。CR2用于發(fā)生頁(yè)異常時(shí)報(bào)告出錯(cuò)信息。當(dāng)發(fā)生頁(yè)異常時(shí),處理器把引起頁(yè)異常的線性地址保存在CR2中。操作系統(tǒng)中對(duì)應(yīng)的中斷服務(wù)例程可以檢查CR2的內(nèi)容,從而查出線性地址空間中的哪個(gè)頁(yè)引起本次異常。

產(chǎn)生頁(yè)訪問(wèn)異常后,CPU硬件和軟件都會(huì)做一些事情來(lái)應(yīng)對(duì)此事。首先頁(yè)訪問(wèn)異常也是一種異常,所以針對(duì)一般異常的硬件處理操作是必須要做的,即CPU在當(dāng)前內(nèi)核棧保存當(dāng)前被打斷的程序現(xiàn)場(chǎng),即依次壓入當(dāng)前被打斷程序使用的EFLAGS,CS,EIP,errorCode;由于頁(yè)訪問(wèn)異常的中斷號(hào)是0xE,CPU把異常中斷號(hào)0xE對(duì)應(yīng)的中斷服務(wù)例程的地址(vectors.S中的標(biāo)號(hào)vector14處)加載到CS和EIP寄存器中,開(kāi)始執(zhí)行中斷服務(wù)例程。這時(shí)ucore開(kāi)始處理異常中斷,首先需要保存硬件沒(méi)有保存的寄存器。

在vectors.S中的標(biāo)號(hào)vector14處先把中斷號(hào)壓入內(nèi)核棧,然后再在trapentry.S中的標(biāo)號(hào)__alltraps處把DS、ES和其他通用寄存器都?jí)簵!W源?,被打斷的程序?zhí)行現(xiàn)場(chǎng)(context)被保存在內(nèi)核棧中。接下來(lái),在trap.c的trap函數(shù)開(kāi)始了中斷服務(wù)例程的處理流程,大致調(diào)用關(guān)系為:

trap—> trap_dispatch—>pgfault_handler—>do_pgfault

在操作系統(tǒng)中,do_pgfault(頁(yè)錯(cuò)誤處理函數(shù))是由內(nèi)核調(diào)用的函數(shù)。當(dāng)程序訪問(wèn)一個(gè)尚未映射到物理內(nèi)存的頁(yè)面時(shí),會(huì)觸發(fā)頁(yè)錯(cuò)誤異常。此時(shí),操作系統(tǒng)會(huì)捕獲這個(gè)異常,并將控制權(quán)轉(zhuǎn)移到do_pgfault函數(shù)中進(jìn)行處理。

具體的調(diào)用關(guān)系可能因不同的操作系統(tǒng)和架構(gòu)而有所差異,以下是一般情況下的調(diào)用關(guān)系:

  • 當(dāng)發(fā)生頁(yè)錯(cuò)誤時(shí),CPU會(huì)產(chǎn)生一個(gè)異常,并將控制權(quán)交給操作系統(tǒng)。
  • 操作系統(tǒng)根據(jù)異常類型確定是否為頁(yè)錯(cuò)誤,并檢查導(dǎo)致頁(yè)錯(cuò)誤的原因。
  • 如果是頁(yè)面訪問(wèn)權(quán)限問(wèn)題或者缺頁(yè)(頁(yè)面尚未加載到物理內(nèi)存)等原因引起的頁(yè)錯(cuò)誤,則執(zhí)行相應(yīng)的處理邏輯。
  • 在處理邏輯中,操作系統(tǒng)可能需要分配物理內(nèi)存來(lái)滿足缺頁(yè)請(qǐng)求,并將相應(yīng)的頁(yè)面加載到物理內(nèi)存中。
  • 執(zhí)行完必要的處理后,操作系統(tǒng)將重新設(shè)置相關(guān)寄存器和標(biāo)志位,然后恢復(fù)被中斷的進(jìn)程繼續(xù)執(zhí)行。

總之,do_pgfault函數(shù)作為內(nèi)核提供的一個(gè)重要回調(diào)函數(shù),在頁(yè)錯(cuò)誤發(fā)生時(shí)負(fù)責(zé)處理該異常并采取必要措施

產(chǎn)生頁(yè)訪問(wèn)異常后,CPU把引起頁(yè)訪問(wèn)異常的線性地址裝到寄存器CR2中,并給出了出錯(cuò)碼errorCode,說(shuō)明了頁(yè)訪問(wèn)異常的類型。ucore OS會(huì)把這個(gè)值保存在struct trapframe 中tf_err成員變量中。而中斷服務(wù)例程會(huì)調(diào)用頁(yè)訪問(wèn)異常處理函數(shù)do_pgfault進(jìn)行具體處理。這里的頁(yè)訪問(wèn)異常處理是實(shí)現(xiàn)按需分頁(yè)、頁(yè)換入換出機(jī)制的關(guān)鍵之處。

ucore中do_pgfault函數(shù)是完成頁(yè)訪問(wèn)異常處理的主要函數(shù),它根據(jù)從CPU的控制寄存器CR2中獲取的頁(yè)訪問(wèn)異常的物理地址以及根據(jù)errorCode的錯(cuò)誤類型來(lái)查找此地址是否在某個(gè)VMA的地址范圍內(nèi)以及是否滿足正確的讀寫(xiě)權(quán)限,如果在此范圍內(nèi)并且權(quán)限也正確,這認(rèn)為這是一次合法訪問(wèn),但沒(méi)有建立虛實(shí)對(duì)應(yīng)關(guān)系。所以需要分配一個(gè)空閑的內(nèi)存頁(yè),并修改頁(yè)表完成虛地址到物理地址的映射,刷新TLB,然后調(diào)用iret中斷,返回到產(chǎn)生頁(yè)訪問(wèn)異常的指令處重新執(zhí)行此指令。如果該虛地址不在某VMA范圍內(nèi),則認(rèn)為是一次非法訪問(wèn)。

1. 缺頁(yè)錯(cuò)誤的分類處理

我們?cè)谇白鲀?nèi)存尋址中介紹了 CPU 發(fā)展過(guò)程中內(nèi)存尋址方式的變化?,F(xiàn)代 CPU 都支持分段和分頁(yè)的內(nèi)存尋址模式。出于尋址能力的考慮,現(xiàn)代操作系統(tǒng),也順應(yīng)著都支持段頁(yè)式的內(nèi)存管理模式。當(dāng)然,雖然支持段頁(yè)式,但是 Linux 中只啟用了段基址為 0 的段。也就是說(shuō),在 Linux 當(dāng)中,實(shí)際起作用的只有分頁(yè)模式。

具體來(lái)說(shuō),分頁(yè)模式在邏輯上將虛擬內(nèi)存和物理內(nèi)存同時(shí)等分成固定大小的塊。這些塊在虛擬內(nèi)存上稱之為「頁(yè)」,而在物理內(nèi)存上稱之為「頁(yè)幀」,并交由 CPU 中的 MMU 模塊來(lái)負(fù)責(zé)頁(yè)幀和頁(yè)之間的映射管理。

引入分頁(yè)模式的好處,可以大致概括為兩個(gè)方面:

  • 允許虛存空間遠(yuǎn)大于實(shí)際物理內(nèi)存大小的情況。這是因?yàn)?,分?yè)之后,操作系統(tǒng)讀入磁盤的文件時(shí),無(wú)需以文件為單位全部讀入,而可以以內(nèi)存頁(yè)為單位,分片讀入。同時(shí),考慮到 CPU 不可能一次性需要使用整個(gè)內(nèi)存中的數(shù)據(jù),因此可以交由特定的算法,進(jìn)行內(nèi)存調(diào)度:將長(zhǎng)時(shí)間不用的頁(yè)幀內(nèi)的數(shù)據(jù)暫存到磁盤上。
  • 減少了內(nèi)存碎片的產(chǎn)生。這是因?yàn)?,引入分?yè)之后,內(nèi)存的分配管理都是以頁(yè)大小(通常是 4KiB,擴(kuò)展分頁(yè)模式下是 4MiB)為單位的;虛擬內(nèi)存中的頁(yè)總是對(duì)應(yīng)物理內(nèi)存中實(shí)際的頁(yè)幀。這樣一來(lái),在虛擬內(nèi)存空間中,頁(yè)內(nèi)連續(xù)的內(nèi)存在物理內(nèi)存上也一定是連續(xù)的,不會(huì)產(chǎn)生碎片。

當(dāng)進(jìn)程在進(jìn)行一些計(jì)算時(shí),CPU 會(huì)請(qǐng)求內(nèi)存中存儲(chǔ)的數(shù)據(jù)。在這個(gè)請(qǐng)求過(guò)程中,CPU 發(fā)出的地址是邏輯地址(虛擬地址),然后交由 CPU 當(dāng)中的 MMU 單元進(jìn)行內(nèi)存尋址,找到實(shí)際物理內(nèi)存上的內(nèi)容。若是目標(biāo)虛存空間中的內(nèi)存頁(yè)(因?yàn)槟撤N原因),在物理內(nèi)存中沒(méi)有對(duì)應(yīng)的頁(yè)幀,那么 CPU 就無(wú)法獲取數(shù)據(jù)。這種情況下,CPU 是無(wú)法進(jìn)行計(jì)算的,于是它就會(huì)報(bào)告一個(gè)缺頁(yè)錯(cuò)誤(Page Fault)。

因?yàn)?CPU 無(wú)法繼續(xù)進(jìn)行進(jìn)程請(qǐng)求的計(jì)算,并報(bào)告了缺頁(yè)錯(cuò)誤,用戶進(jìn)程必然就中斷了。這樣的中斷稱之為缺頁(yè)中斷。在報(bào)告 Page Fault 之后,進(jìn)程會(huì)從用戶態(tài)切換到系統(tǒng)態(tài),交由操作系統(tǒng)內(nèi)核的 Page Fault Handler 處理缺頁(yè)錯(cuò)誤。

基本來(lái)說(shuō),缺頁(yè)錯(cuò)誤可以分為兩類:硬缺頁(yè)錯(cuò)誤(Hard Page Fault)和軟缺頁(yè)錯(cuò)誤(Soft Page Fault)。這里,前者又稱為主要缺頁(yè)錯(cuò)誤(Major Page Fault);后者又稱為次要缺頁(yè)錯(cuò)誤(Minor Page Fault)。當(dāng)缺頁(yè)中斷發(fā)生后,Page Fault Handler 會(huì)判斷缺頁(yè)的類型,進(jìn)而處理缺頁(yè)錯(cuò)誤,最終將控制權(quán)交給用戶態(tài)代碼。

若是此時(shí)物理內(nèi)存里,已經(jīng)有一個(gè)頁(yè)幀正是此時(shí) CPU 請(qǐng)求的內(nèi)存頁(yè),那么這是一個(gè)軟缺頁(yè)錯(cuò)誤;于是,Page Fault Hander 會(huì)指示 MMU 建立相應(yīng)的頁(yè)幀到頁(yè)的映射關(guān)系。這一操作的實(shí)質(zhì)是進(jìn)程間共享內(nèi)存——比如動(dòng)態(tài)庫(kù)(共享對(duì)象),比如 mmap 的文件。

若是此時(shí)物理內(nèi)存中,沒(méi)有相應(yīng)的頁(yè)幀,那么這就是一個(gè)硬缺頁(yè)錯(cuò)誤;于是 Page Fault Hander 會(huì)指示 CPU,從已經(jīng)打開(kāi)的磁盤文件中讀取相應(yīng)的內(nèi)容到物理內(nèi)存,而后交由 MMU 建立這份頁(yè)幀到頁(yè)的映射關(guān)系。

不難發(fā)現(xiàn),軟缺頁(yè)錯(cuò)誤只是在內(nèi)核態(tài)里輕輕地走了一遭,而硬缺頁(yè)錯(cuò)誤則涉及到磁盤 I/O。因此,處理起來(lái),硬缺頁(yè)錯(cuò)誤要比軟缺頁(yè)錯(cuò)誤耗時(shí)長(zhǎng)得多。這就是為什么我們要求高性能程序必須在對(duì)外提供服務(wù)時(shí),盡可能少地發(fā)生硬缺頁(yè)錯(cuò)誤。

除了硬缺頁(yè)錯(cuò)誤和軟缺頁(yè)錯(cuò)誤之外,還有一類缺頁(yè)錯(cuò)誤是因?yàn)樵L問(wèn)非法內(nèi)存引起的。前兩類缺頁(yè)錯(cuò)誤中,進(jìn)程嘗試訪問(wèn)的虛存地址尚為合法有效的地址,只是對(duì)應(yīng)的物理內(nèi)存頁(yè)幀沒(méi)有在物理內(nèi)存當(dāng)中。后者則不然,進(jìn)程嘗試訪問(wèn)的虛存地址是非法無(wú)效的地址。比如嘗試對(duì) nullptr 解引用,就會(huì)訪問(wèn)地址為 0x0 的虛存地址,這是非法地址。

此時(shí) CPU 報(bào)出無(wú)效缺頁(yè)錯(cuò)誤(Invalid Page Fault)。操作系統(tǒng)對(duì)無(wú)效缺頁(yè)錯(cuò)誤的處理各不相同:Windows 會(huì)使用異常機(jī)制向進(jìn)程報(bào)告;*nix 則會(huì)通過(guò)向進(jìn)程發(fā)送 SIGSEGV 信號(hào)(11),引發(fā)內(nèi)存轉(zhuǎn)儲(chǔ)。

缺頁(yè)中斷會(huì)交給PageFaultHandler處理,其根據(jù)缺頁(yè)中斷的不同類型會(huì)進(jìn)行不同的處理:

  • Hard Page Fault:也被稱為Major Page Fault,翻譯為硬缺頁(yè)錯(cuò)誤/主要缺頁(yè)錯(cuò)誤,這時(shí)物理內(nèi)存中沒(méi)有對(duì)應(yīng)的頁(yè)幀,需要CPU打開(kāi)磁盤設(shè)備讀取到物理內(nèi)存中,再讓MMU建立VA和PA的映射。
  • Soft Page Fault:也被稱為Minor Page Fault,翻譯為軟缺頁(yè)錯(cuò)誤/次要缺頁(yè)錯(cuò)誤,這時(shí)物理內(nèi)存中是存在對(duì)應(yīng)頁(yè)幀的,只不過(guò)可能是其他進(jìn)程調(diào)入的,發(fā)出缺頁(yè)異常的進(jìn)程不知道而已,此時(shí)MMU只需要建立映射即可,無(wú)需從磁盤讀取寫(xiě)入內(nèi)存,一般出現(xiàn)在多進(jìn)程共享內(nèi)存區(qū)域。
  • Invalid Page Fault:翻譯為無(wú)效缺頁(yè)錯(cuò)誤,比如進(jìn)程訪問(wèn)的內(nèi)存地址越界訪問(wèn),又比如對(duì)空指針解引用內(nèi)核就會(huì)報(bào)segment fault錯(cuò)誤中斷進(jìn)程直接掛掉。

2. 缺頁(yè)錯(cuò)誤出現(xiàn)的原因

(1) 頁(yè)表相關(guān)問(wèn)題

當(dāng)進(jìn)程訪問(wèn)虛擬地址時(shí),首先會(huì)查詢頁(yè)表以獲取對(duì)應(yīng)的物理地址。如果頁(yè)表中找不到對(duì)應(yīng)虛擬地址的頁(yè)表項(xiàng)(Page Table Entry,PTE),就會(huì)觸發(fā)頁(yè)異常 。這可能是因?yàn)樵撎摂M地址是無(wú)效的,就像你在一個(gè)公司的員工名單(頁(yè)表)中查找一個(gè)根本不存在的員工的工號(hào)(虛擬地址),自然是找不到的。

另一種情況是,虛擬地址是有效的,但對(duì)應(yīng)的物理頁(yè)面尚未被載入主存,頁(yè)表項(xiàng)還沒(méi)有建立,就好比員工雖然存在,但還沒(méi)有分配工位(物理內(nèi)存位置),在名單上也沒(méi)有記錄其工位信息。比如在程序動(dòng)態(tài)分配內(nèi)存時(shí),malloc 函數(shù)只是在虛擬地址空間中預(yù)留了一段地址范圍,當(dāng)進(jìn)程首次訪問(wèn)這段地址時(shí),由于對(duì)應(yīng)的物理內(nèi)存尚未分配和映射,就會(huì)導(dǎo)致頁(yè)表中沒(méi)有相應(yīng)的 PTE,從而觸發(fā)頁(yè)異常 。

(2)訪問(wèn)權(quán)限沖突

即使頁(yè)表中存在對(duì)應(yīng)虛擬地址的 PTE,但如果該 PTE 的訪問(wèn)權(quán)限設(shè)置拒絕當(dāng)前進(jìn)程的訪問(wèn)操作,也會(huì)引發(fā)頁(yè)異常。例如,一個(gè)進(jìn)程試圖對(duì)一個(gè)只讀的頁(yè)面進(jìn)行寫(xiě)入操作,而該頁(yè)面的 PTE 中設(shè)置了只讀權(quán)限,這就好比你拿著一張只能進(jìn)入圖書(shū)館閱讀區(qū)的通行證,卻試圖進(jìn)入書(shū)庫(kù)(禁止區(qū)域)取書(shū),自然會(huì)被拒絕并觸發(fā)異常。這種情況通常用于保護(hù)系統(tǒng)關(guān)鍵數(shù)據(jù)和代碼,防止進(jìn)程的非法訪問(wèn)和修改,保障系統(tǒng)的穩(wěn)定性和安全性。

四、處理流程全解析

1. 捕獲與跳轉(zhuǎn)

當(dāng)頁(yè)異常發(fā)生時(shí),首先由 CPU 捕獲這個(gè)異常信號(hào)。就像在一個(gè)公司里,員工(進(jìn)程)在執(zhí)行任務(wù)(訪問(wèn)內(nèi)存)時(shí)遇到問(wèn)題(頁(yè)異常),會(huì)立即向上級(jí)(CPU)報(bào)告 。CPU 捕獲到這個(gè)異常后,會(huì)跳轉(zhuǎn)到專門處理頁(yè)異常的函數(shù)page_fault_handler,這個(gè)函數(shù)就像是公司里專門處理問(wèn)題的部門,負(fù)責(zé)解決內(nèi)存訪問(wèn)異常的問(wèn)題 。在page_fault_handler中,會(huì)進(jìn)一步分析異常的原因和類型,為后續(xù)的處理做準(zhǔn)備。

2. 處理邏輯分支

(1) 無(wú)效地址處理

如果經(jīng)檢查發(fā)現(xiàn)訪問(wèn)的地址是無(wú)效地址,屬于越界訪問(wèn),系統(tǒng)會(huì)返回segment fault錯(cuò)誤 。這就好比員工試圖進(jìn)入一個(gè)被禁止進(jìn)入的辦公室(無(wú)效地址區(qū)域),會(huì)被保安(系統(tǒng))阻止并報(bào)告給上級(jí)。如果是用戶地址發(fā)生segment fault,系統(tǒng)會(huì)直接殺死該進(jìn)程,以防止進(jìn)程對(duì)系統(tǒng)造成進(jìn)一步的破壞,就像公司會(huì)開(kāi)除違反規(guī)定的員工 。而如果是內(nèi)核地址發(fā)生segment fault,情況則更為嚴(yán)重,可能會(huì)導(dǎo)致內(nèi)核崩潰,就像公司的核心管理層出現(xiàn)嚴(yán)重問(wèn)題,可能會(huì)導(dǎo)致整個(gè)公司運(yùn)營(yíng)癱瘓 。

(2) 有效地址處理

首次訪問(wèn):當(dāng)頁(yè)是第一次被訪問(wèn)時(shí),會(huì)執(zhí)行demand_page_faults(請(qǐng)求調(diào)頁(yè))操作 。這時(shí)候,系統(tǒng)會(huì)檢查頁(yè)表中是否存在該虛擬地址對(duì)應(yīng)的頁(yè)表項(xiàng)(PTE),即通過(guò)pmd_none和pte_none等函數(shù)來(lái)判斷 。如果不存在,就需要分配新的頁(yè)幀,并對(duì)其進(jìn)行初始化,從磁盤中讀取相應(yīng)的數(shù)據(jù)到內(nèi)存中 。這就像是公司要為新入職的員工(新訪問(wèn)的頁(yè))分配一個(gè)工位(頁(yè)幀),并從倉(cāng)庫(kù)(磁盤)中取出相關(guān)的辦公用品(數(shù)據(jù))放到工位上,以便員工能夠正常開(kāi)展工作(進(jìn)程能夠正常訪問(wèn)內(nèi)存) 。

頁(yè)在 swap 分區(qū):如果頁(yè)被交換到了 swap 分區(qū),系統(tǒng)會(huì)檢查頁(yè)表項(xiàng)中的present標(biāo)志位 。這個(gè)標(biāo)志位就像是一個(gè)標(biāo)簽,用來(lái)標(biāo)識(shí)頁(yè)面是否在主存中 。如果present標(biāo)志位為 0,表示該頁(yè)不在主存中,此時(shí)需要分配新的頁(yè)幀,并從磁盤的 swap 分區(qū)重新讀入內(nèi)存 。這就好比員工的辦公用品被暫時(shí)存放到了倉(cāng)庫(kù)的臨時(shí)存儲(chǔ)區(qū)(swap 分區(qū)),當(dāng)員工需要使用時(shí),需要從臨時(shí)存儲(chǔ)區(qū)把辦公用品取回到工位(內(nèi)存)上 。

COW 情況:當(dāng)vm_area_struct允許寫(xiě)操作,但對(duì)應(yīng)的頁(yè)表項(xiàng)(PTE)禁止寫(xiě)操作時(shí),就會(huì)觸發(fā)寫(xiě)時(shí)復(fù)制(Copy-On-Write,COW)機(jī)制 。這是一種優(yōu)化策略,在多個(gè)進(jìn)程共享同一個(gè)頁(yè)面時(shí),只有當(dāng)某個(gè)進(jìn)程試圖對(duì)頁(yè)面進(jìn)行寫(xiě)操作時(shí),才會(huì)真正復(fù)制出一個(gè)新的頁(yè)面供該進(jìn)程使用,而不是在一開(kāi)始就為每個(gè)進(jìn)程都復(fù)制一份 。就好比多個(gè)員工共同使用一份文件(共享頁(yè)面),當(dāng)其中一個(gè)員工想要修改文件內(nèi)容(寫(xiě)操作)時(shí),系統(tǒng)會(huì)為他復(fù)制一份文件副本(新頁(yè)面),讓他在副本上進(jìn)行修改,而不影響其他員工使用的原始文件 。這樣可以節(jié)省內(nèi)存資源,提高系統(tǒng)的效率 。

五、實(shí)例與應(yīng)用場(chǎng)景

1. 程序運(yùn)行中的體現(xiàn)

以一個(gè)簡(jiǎn)單的 C 程序?yàn)槔?,?dāng)程序執(zhí)行到malloc函數(shù)分配內(nèi)存時(shí),實(shí)際上只是在虛擬地址空間中預(yù)留了一段地址范圍,并沒(méi)有立即分配物理內(nèi)存。只有當(dāng)程序首次訪問(wèn)這段地址時(shí),才會(huì)觸發(fā)頁(yè)異常 。假設(shè)我們有如下 C 代碼:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = (int *)malloc(1024 * sizeof(int)); // 分配內(nèi)存,但未實(shí)際占用物理內(nèi)存
    if (ptr == NULL) {
        perror("malloc failed");
        return 1;
    }

    // 首次訪問(wèn)分配的內(nèi)存,會(huì)觸發(fā)頁(yè)異常
    ptr[0] = 100; 

    printf("Value at ptr[0]: %d\n", ptr[0]);
    free(ptr);
    return 0;
}

在這段代碼中,malloc函數(shù)分配了一段虛擬內(nèi)存地址,當(dāng)執(zhí)行ptr[0] = 100;時(shí),由于這是首次訪問(wèn)該地址,對(duì)應(yīng)的物理內(nèi)存尚未分配和映射,系統(tǒng)會(huì)觸發(fā)頁(yè)異常。操作系統(tǒng)會(huì)捕獲這個(gè)異常,為該虛擬地址分配物理內(nèi)存頁(yè)幀,并建立虛擬地址與物理地址的映射關(guān)系,然后程序才能成功地將值 100 寫(xiě)入ptr[0] 。

2. 系統(tǒng)性能影響

頁(yè)異常對(duì)系統(tǒng)性能有著顯著的影響,尤其是硬缺頁(yè)。由于硬缺頁(yè)需要從磁盤讀取數(shù)據(jù)到內(nèi)存,而磁盤 I/O 的速度遠(yuǎn)遠(yuǎn)慢于內(nèi)存訪問(wèn)速度,頻繁的硬缺頁(yè)會(huì)導(dǎo)致系統(tǒng)性能大幅下降 。當(dāng)一個(gè)進(jìn)程頻繁地訪問(wèn)大量數(shù)據(jù),而這些數(shù)據(jù)又不在物理內(nèi)存中時(shí),就會(huì)不斷地觸發(fā)硬缺頁(yè)。比如一個(gè)數(shù)據(jù)庫(kù)管理系統(tǒng)在處理大量數(shù)據(jù)查詢時(shí),如果內(nèi)存不足,無(wú)法緩存所有需要的數(shù)據(jù),就會(huì)頻繁地從磁盤讀取數(shù)據(jù),導(dǎo)致大量的硬缺頁(yè)發(fā)生。這不僅會(huì)增加磁盤 I/O 的負(fù)擔(dān),還會(huì)使進(jìn)程的執(zhí)行速度明顯變慢,進(jìn)而影響整個(gè)系統(tǒng)的響應(yīng)速度和吞吐量。

相比之下,軟缺頁(yè)的影響相對(duì)較小,因?yàn)樗恍枰獜拇疟P讀取數(shù)據(jù),只是建立映射關(guān)系,這個(gè)過(guò)程相對(duì)快速。但如果軟缺頁(yè)過(guò)于頻繁,也會(huì)消耗一定的系統(tǒng)資源,影響系統(tǒng)的整體性能 。例如,在一個(gè)多進(jìn)程共享內(nèi)存的場(chǎng)景中,如果進(jìn)程之間頻繁地訪問(wèn)共享內(nèi)存,可能會(huì)導(dǎo)致大量的軟缺頁(yè),因?yàn)槊總€(gè)進(jìn)程在首次訪問(wèn)共享內(nèi)存時(shí)都需要建立映射關(guān)系。雖然單個(gè)軟缺頁(yè)的處理時(shí)間較短,但大量的軟缺頁(yè)累積起來(lái),也會(huì)對(duì)系統(tǒng)性能產(chǎn)生一定的影響。

責(zé)任編輯:趙寧寧 來(lái)源: 深度Linux
相關(guān)推薦

2025-03-25 01:00:00

2024-11-08 13:47:35

中文亂碼配置

2018-10-12 14:34:13

2010-04-26 16:31:09

Oracle SQL

2011-05-24 11:26:11

2011-06-29 08:31:56

收錄

2010-05-12 14:18:58

Linux引導(dǎo)

2010-05-21 13:05:41

MySQL錯(cuò)誤

2010-08-31 09:06:25

Firefox margin-top

2025-01-07 16:00:00

Kubernetes云原生Pod

2024-09-26 00:00:10

死鎖阿里面試

2015-07-27 16:56:24

LinuxQQ

2010-01-22 15:42:01

VB.NET錯(cuò)誤

2021-07-05 08:09:54

@AutowiredSpringMapper

2018-08-21 09:03:00

SQLServer服務(wù)器

2024-08-28 08:54:54

2021-06-30 07:08:14

安全解決方案XDR安全技術(shù)

2009-07-22 17:37:06

ASP.NET Ses

2010-05-26 13:14:22

MySQL錯(cuò)誤解決方案

2010-03-26 18:41:51

Nginx 502錯(cuò)誤
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)