<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    so true

    心懷未來,開創未來!
    隨筆 - 160, 文章 - 0, 評論 - 40, 引用 - 0
    數據加載中……

    進程的虛擬地址空間

    昨晚看到了深夜,終于對進程的虛擬地址空間有了個大致的了解,很激動,也很欣慰。回頭想來,一個程序員,真的應該知道這些知識,否則還真不太稱職。
    首先告訴大家,我后面提到的這些知識在《windows核心編程》中都有,強烈建議大家把這本書翻翻,我相信會對你的編程境界拔高好幾個層次的。可是我最近沒那么多時間,因此就只能了解個大概,然后等今后閑暇時再看這本書吧。
    昨天我媳婦還反復和我說:學東西必須要有選擇,不能對IT行業的所有知識亂學習,而且不要學那種實際意義不大的知識或是容易被淘汰的知識。其實她說的蠻對的,但是我要說,有關《windows核心編程》里的知識永遠都不會過時,因為它侵入到底層和內部了,就像C++,你覺得會過時嗎?就像windows永遠不會被淘汰一樣,呵呵。

    下面我就來粗略的說說我了解的一些基本知識:
    32位機器,每個程序有4G的虛擬地址空間。大致分為4塊,從低地址到高地址依次是:NULL區,用戶區,隔離區,核心區。用戶私有的數據都在用戶區(當然這個區里又可以細分,其中也包括一部分可以共享的內容),系統內核等東西都在核心區。總體來說,A進程的虛擬地址空間中的內容和B進程相比,只有各自的用戶區不一致。通常用戶區中,進程又會將exe文件(由頭數據和段數據組成)中定義的代碼段、堆棧段、數據段等各個段映射到用戶區的特定不同部位。對于這部分區域,用戶需要用VirtualAlloc先為自己預留后再提交,最后在自己的頁面被cpu訪問時再從exe映像中將數據加載到主存,然后將虛擬地址映射為主存的物理地址。基本上這樣就可以了,至于系統如何進行頁面的管理以及地址映射如何實現等細節請大家再參考別的文獻。

    我本以為很復雜呢,結果寫出來,就這么一小段,呵呵,看來是高估了自己理解的東西了,呵呵。

    下面貼出我看的一些資料:

    虛擬存儲器是一個抽象概念,它為每一個進程提供了一個假象,好像每個進程都在獨占的使用主存。每個進程看到的存儲器都是一致的,稱之為虛擬地址空間。

         每個進程看到得虛擬地址空間有大量準確定義的區(area)構成,每個區都有專門的功能。從最低的地址看起

    • 程序代碼和數據:代碼是從同一固定地址開始,緊接著的是和C全局變量相對應的數據區。 (應該就是所謂的靜態存儲空間)
    • 堆:代碼和數據區后緊隨著的是運行時堆。作為調用mallocfree這樣的C標準庫函數,堆可以在運行時動態的擴展和收縮。(應該就是所謂的動態存儲區)
    • 共享庫:在地址空間的中間附近是一塊用來存放像C標準庫和數學庫這樣共享庫的代碼和數據的區域。(C標準庫函數的指令,連接階段把他們加入到編譯后的程序)
    • :位于用戶虛擬地址空間頂部的是用戶,編譯器用它來實現函數調用。和堆一樣每次我們從函數返回時,就會收縮。
    • 內核虛擬存儲器:內核是操作系統總是駐留在存儲器中的部分。地址空間頂部的四分之一部分是為內核預留的。(系統函數?這里說的UNIX系統,不知道windows下是不是這樣的?)

         今天大多數計算機的字長都是32字節,這就限制了虛擬地址空間為4千兆字節(4GB

    引言

      Windows的內存結構是深入理解Windows操作系統如何運作的關鍵之所在,通過對內存結構的認識可清楚地了解諸如進程間數據的共享、對內存進行有效的管理等問題,從而能夠在程序設計時使程序以更加有效的方式運行。Windows操作系統對內存的管理可采取多種不同的方式,其中虛擬內存的管理方式可用來管理大型的對象和結構數組。

      在Windows系統中,任何一個進程都被賦予其自己的虛擬地址空間,該虛擬地址空間覆蓋了一個相當大的范圍,對于32位進程,其地址空間為232=4,294,967,296 Byte,這使得一個指針可以使用從0x000000000xFFFFFFFF4GB范圍之內的任何一個值。雖然每一個32位進程可使用4GB的地址空間,但并不意味著每一個進程實際擁有4GB的物理地址空間,該地址空間僅僅是一個虛擬地址空間,此虛擬地址空間只是內存地址的一個范圍。進程實際可以得到的物理內存要遠小于其虛擬地址空間。進程的虛擬地址空間是為每個進程所私有的,在進程內運行的線程對內存空間的訪問都被限制在調用進程之內,而不能訪問屬于其他進程的內存空間。這樣,在不同的進程中可以使用相同地址的指針來指向屬于各自調用進程的內容而不會由此引起混亂。下面分別對虛擬內存的各具體技術進行介紹。
    地址空間中區域的保留與釋放

    在進程創建之初并被賦予地址空間時,其虛擬地址空間尚未分配,處于空閑狀態。這時地址空間內的內存是不能使用的,必須首先通過VirtualAlloc()函數來分配其內的各個區域,對其進行保留

    LPVOID VirtualAlloc(
     LPVOID lpAddress,
     DWORD dwSize,
     DWORD flAllocationType,
     DWORD flProtect
    );

    其參數lpAddress包含一個內存地址,用于定義待分配區域的首地址。通常可將此參數設置為NULL,由系統通過搜索地址空間來決定滿足條件的未保留地址空間。這時系統可從地址空間的任意位置處開始保留一個區域,而且還可以通過向參數flAllocationType設置MEM_TOP_DOWN標志來指明在盡可能高的地址上分配內存。如果不希望由系統自動完成對內存區域的分配而為lpAddress設定了內存地址(必須確保其始終位于進程的用戶模式分區中,否則將會導致分配的失敗),那么系統將在進行分配之前首先檢查在該內存地址上是否存在足夠大的未保留空間,如果存在一個足夠大的空閑區域,那么系統將會保留此區域并返回此保留區域的虛擬地址,否則將導致分配的失敗而返回NULL。這里需要特別指出的是,在指定lpAddress的內存地址時,必須確保是從一個分配粒度的邊界處開始。
    一般來說,在不同的CPU平臺下分配粒度各不相同,但目前所有Windows環境下的CPUx8632Alpha64Alpha以及IA-64等均是采用64KB的分配粒度。如果保留區域的起始地址沒有遵循從64KB分配粒度的邊界開始之一原則,系統將自動調整該地址到最接近的64K的倍數。例如,如果指定的lpAddress0x00781022,那么此保留區域實際是從0x00780000開始分配的。參數dwSize指定了保留區域的大小。但是系統實際保留的區域大小必須是CPU頁面大小的整數,如果指定的dwSize并非CPU頁面的整數系統將自動對其進行調整,使其達到與之最接近的頁面大小整數與分配粒度一樣,對于不同的CPU平臺其頁面大小也是不一樣的。在x86平臺下,頁面大小為4KB,在32Alpah平臺下,頁面大小為8KB。在使用時可以通過GetSystemInfo()來決定當前主機的頁面大小。參數flAllocationTypeflProtect分別定義了分配類型和訪問保護屬性。由于VirtualAlloc()可用來保留一個區域也可以用來占用物理存儲器,因此通過flAllocationType來指定當前是要保留一個區域還是要占用物理存儲器。其可能使用的內存分配類型有:

    分配類型

    類型說明

    MEM_COMMIT

    為特定的頁面區域分配內存中或磁盤的頁面文件中的物理存儲

    MEM_PHYSICAL

    分配物理內存(僅用于地址窗口擴展內存)

    MEM_RESERVE

    保留進程的虛擬地址空間,而不分配任何物理存儲。保留頁面可通過繼續調用VirtualAlloc()而被占用

    MEM_RESET

    指明在內存中由參數lpAddressdwSize指定的數據無效

    MEM_TOP_DOWN

    在盡可能高的地址上分配內存(Windows 98忽略此標志)

    MEM_WRITE_WATCH

    必須與MEM_RESERVE一起指定,使系統跟蹤那些被寫入分配區域的頁面(僅針對Windows 98


      分配成功完成后,即在進程的虛擬地址空間中保留了一個區域,可以對此區域中的內存進行保護權限許可范圍內的訪問。當不再需要訪問此地址空間區域時,應釋放此區域。由VirtualFree()負責完成。其函數原型為:

    BOOL VirtualFree(
     LPVOID lpAddress,
     DWORD dwSize,
     DWORD dwFreeType
    );

    其中,參數lpAddress為指向待釋放頁面區域的指針。如果參數dwFreeType指定了MEM_RELEASE,則lpAddress必須為頁面區域被保留時由VirtualAlloc()所返回的基地址。參數dwSize指定了要釋放的地址空間區域的大小,如果參數dwFreeType指定了MEM_RELEASE標志,則將dwSize設置為0,由系統計算在特定內存地址上的待釋放區域的大小。參數dwFreeType為所執行的釋放操作的類型,其可能的取值為MEM_RELEASEMEM_DECOMMIT,其中MEM_RELEASE標志指明要釋放指定的保留頁面區域,MEM_DECOMMIT標志則對指定的占用頁面區域進行占用的解除。如果VirtualFree()成功執行完成,將回收全部范圍的已分配頁面,此后如再對這些已釋放頁面區域內存的訪問將引發內存訪問異常。釋放后的頁面區域可供系統繼續分配使用。

      下面這段代碼演示了由系統在進程的用戶模式分區內保留一個64KB大小的區域,并將其釋放的過程:

    // 在地址空間中保留一個區域

    LPBYTE bBuffer = (LPBYTE)VirtualAlloc(NULL, 65536, MEM_RESERVE, PAGE_READWRITE);

    ……

    // 釋放已保留的區域

    VirtualFree(bBuffer, 0, MEM_RELEASE);

    flProtect頁面保護屬性

    我們可以給每個已分配的物理存儲頁指定不同的頁面保護屬性。表13-3列出了所有的頁面保護屬性。

    13-3  內存頁面保護屬性

    保護屬性

     

    PAGE_NOACCESS

    試圖讀取頁面、寫入頁面或執行頁面中的代碼將引發訪問違規

    PAGE_READONLY

    試圖寫入頁面或執行頁面中的代碼將引發訪問違規

    PAGE_READWRITE

    試圖執行頁面中的代碼將引發訪問違規

    PAGE_EXECUTE

    試圖讀取頁面或寫入頁面將引發訪問違規

    PAGE_EXECUTE_READ

    試圖寫入頁面將引發訪問違規

    PAGE_EXECUTE_READWRITE

    對頁面執行任何操作都不會引發訪問違規

    PAGE_WRITECOPY

    試圖執行頁面中的代碼將引發訪問違規。試圖寫入頁面將使系統為進程單獨創建一份該頁面的私有副本(以頁交換文件為后備存儲器)

    PAGE_EXECUTE_WRITECOPY

    對頁面執行任何操作都不會引發訪問違規。試圖寫入頁面將使系統為進程單獨創建一份該頁面的私有副本(以頁交換文件為后備存儲器)

    一些惡意軟件將代碼寫入到用于數據的內存區域(比如線程),通過這種方式讓應用程序執行惡意代碼。Windows數據執行保護(Data Execution Protection,后面簡稱為DEP)特性提供了對此類惡意攻擊的防護。如果啟用了DEP,那么只有對那些真正需要執行代碼的內存區域,操作系統才會使用PAGE_EXECUTE_*保護屬性。其他保護屬性(最常見的就是PAGE_READWRITE)用于只應該存放數據的內存區域(比如線程和應用程序的堆)

    如果CPU試圖執行某個頁面中的代碼,而該頁又沒有PAGE_EXECUTE_*保護屬性,那么CPU會拋出訪問違規異常。

    系統還對Windows支持的結構化異常處理機制(structured exception handling mechanism)做了更進一步的保護,結構化異常處理機制會在第2325章詳細介紹。如果應用程序在鏈接時使用了/SAFESEH開關,那么異常處理器會被注冊到映像文件中一個特殊的表中。這樣,當將要執行一個異常處理器時,操作系統會先檢查該處理器有沒有在表中注冊過,然后決定是否允許它執行。

    有關DEP的更多信息,請訪問http://go.microsoft.com/fwlink/?LinkId=28022,可以在此找到Microsoft白皮書“03_CIF_Memory_Protection.DOC”。

                                                                           

    13.6.1  寫時復制

    在表13.3中列出的保護屬性中,除最后兩個屬性PAGE_WRITECOPYPAGE_EXECUTE_WRITECOPY之外,其余的都不言自明。這兩個保護屬性存在的目的是為了節省內存和頁交換文件的使用。Windows支持一種機制,允許兩個或兩個以上的進程共享同一塊存儲器。因此,如果有10個記事本程序正在運行,所有的進程會共享應用程序的代碼頁和數據頁。讓所有的應用程序實例共享相同的存儲頁極大地提升了系統的性能,但另一方面,這也要求所有的應用程序實例只能讀取其中的數據或是執行其中的代碼。如果某個應用程序實例修改并寫入一個存儲頁,那么這等于是修改了其他實例正在使用的存儲頁,最終將導致混亂。

    為了避免此類混亂的發生,操作系統會給共享的存儲頁指定寫時復制屬性。當系統把一個.exe.dll映射到一個地址空間的時候,系統會計算有多少頁面是可寫的。(通常,包含代碼的頁面被標記為PAGE_EXECUTE_READ,而包含數據的頁面被標記為PAGE_READWRITE)然后系統會從頁交換文件中分配存儲空間來容納這些可寫頁面。除非應用程序真的寫入可寫頁面,否則不會用到頁交換文件中的存儲器。

    當線程試圖寫入一個共享頁面時,系統會介入并執行下面的操作。

    (1)   系統在內存中找到一個閑置頁面。注意,該閑置頁面的后備頁面來自頁交換文件,它是系統最初將模塊映射到進程的地址空間時分配的。由于系統在第一次進行映射的時候分配了所有可能需要的頁交換文件空間,這一步不可能失敗。

    (2)   系統把線程想要修改的頁面內容復制到在第1步中找到的閑置頁面。系統會給該閑置頁面指定PAGE_READWRITEPAGE_EXECUTE_READWRITE保護屬性,系統不會對原始頁面的保護屬性和數據做任何修改。

    (3)   然后,系統更新進程的頁面表,這樣一來,原來的虛擬地址現在就對應到內存中一個新的頁面了。

    系統在執行這些步驟之后,進程就可以訪問它自己的副本了。第17章將進一步介紹存儲器共享和寫時復制。

    此外,在預訂地址空間或調撥物理存儲器時,不能使用PAGE_WRITECOPYPAGE_EXECUTE_WRITECOPY保護屬性。這樣做會導致調用VirtualAlloc失敗,此時調用GetLastError會返回錯誤碼ERROR_INVALID_PARAMETER。這兩個屬性是操作系統在映射.exeDLL映像文件時用的。

    13.6.2  一些特殊的訪問保護屬性標志

    除了已經介紹過的保護屬性之外,另外還有3個保護屬性標志:PAGE_NOCACHEPAGE_WRITECOMBINEPAGE_GUARD。使用這些標志時,只需將它們與除了PAGE_NOACCESS之外的任何其他保護屬性進行按位或操作即可。

    第一個保護屬性標志PAGE_NOCACHE,用來禁止對已調撥的頁面進行緩存。該標志存在的主要目的是為了讓需要操控內存緩沖區的驅動程序開發人員使用,不建議將該標志用于除此以外的其他用途。

                                                                           

    第二個保護屬性標志PAGE_WRITECOMBINE也是給驅動程序開發人員用的。它允許把對單個設備的多次寫操作組合在一起,以提高性能。

    最后一個保護屬性標志PAGE_GUARD,使應用程序能夠在頁面中的任何一個字節被寫入時得到通知。這個標志有一些巧妙的用法。Windows在創建線程時會用到它。有關該標志的更多信息,請參閱第16章。


    物理存儲器的提交與回收

      在地址空間中保留一個區域后,并不能直接對其進行使用,必須在把物理存儲器提交給該區域后,才可以訪問區域中的內存地址。在提交過程中,物理存儲器是按頁面邊界和頁面大小的塊來進行提交的。若要為一個已保留的地址空間區域提交物理存儲器,需要再次調用VirtualAlloc()函數,所不同的是在執行物理存儲器的提交過程中需要指定flAllocationType參數為MEM_COMMIT標志,使用的保護屬性與保留區域時所用保護屬性一致。在提交時,可以將物理存儲器提交給整個保留區域,也可以進行部分提交,由VirtualAlloc()函數的lpAddress參數和dwSize參數指明要將物理存儲器提交到何處以及要提交多少物理存儲器。
    與保留區域的釋放類似,當不再需要訪問保留區域中被提交的物理存儲器時,提交的物理存儲器應得到及時的釋放。該回收過程與保留區域的釋放一樣也是通過VirtualFree()函數來完成的。在調用時為VirtualFree()的dwFreeType參數指定MEM_DECOMMIT標志,并在參數lpAddressdwSize中傳遞用來標識要解除的第一個頁面的內存地址和釋放的字節數。此回收過程同樣也是以頁面為單位來進行的,將回收設定范圍所涉及到的所有頁面。下面這段代碼演示了對先前保留區域的提交過程,并在使用完畢后將其回收:

    //
    在地址空間中保留一個區域

    LPBYTE bBuffer = (LPBYTE)VirtualAlloc(NULL, 65536, MEM_RESERVE, PAGE_READWRITE);

    // 提交物理存儲器

    VirtualAlloc(bBuffer, 65536, MEM_COMMIT, PAGE_READWRITE);

    ……

    // 回收提交的物理存儲器

    VirtualFree(bBuffer, 65536, MEM_DECOMMIT);

    // 釋放已保留的區域

    VirtualFree(bBuffer, 0, MEM_RELEASE);

     

      由于未經提交的保留區域實際是無法使用的,因此在編程過程中允許通過一次VirtualAlloc()調用而完成對地址空間的區域保留及對保留區域的物理存儲器的提交。相應的,回收、釋放過程也可由一次VirtualFree()調用來實現。上述代碼可按此方法改寫為:

    // 在地址空間中保留一個區域并提交物理存儲器

    LPBYTE bBuffer = (LPBYTE)VirtualAlloc(NULL, 65536, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

    ……

    // 釋放已保留的區域并回收提交的物理存儲器

    VirtualFree(bBuffer, 0, MEM_RELEASE | MEM_DECOMMIT); 

    頁文件的使用

      在前面曾多次提到物理存儲器,這里所說的物理存儲器并不局限于計算機內存,還包括在磁盤空間上創建的頁文件,其存儲空間大小為計算機內存和頁文件存儲容量之。由于通常情況下磁盤存儲空間要遠大于內存的存儲空間,因此頁文件的使用對于應用程序而言相當于透明的增加了其所能使用的內存容量。在使用時,由操作系統和CPU負責對頁文件進行維護和協調。只有在應用程序需要時才臨時將頁文件中的數據加載到內存供應用程序訪問之用,在使用完畢后再從內存交換回頁文件

    進程中的線程在訪問位于已提交物理存儲器的保留區域的內存地址時,如果此地址指向的數據當前已存在于內存,CPU將直接將進程的虛擬地址映射為物理地址,并完成對數據的訪問;如果此數據是存在于頁文件中的,就要試圖將此數據從頁文件加載到內存。在進行此處理時,首先要檢查內存中是否有可供使用的空閑頁面,如果有就可以直接將數據加載到內存中的空閑頁面,否則就要從內存中尋找一個暫不使用的可釋放的頁面并將數據加載到此頁面。如果被釋放頁面中的數據仍為有效數據(即以后還會用到),就要先將此頁面從內存寫入到頁文件。在數據加載到內存后,仍要在CPU將虛擬地址映射為物理地址后方可實現對數據的訪問。與對物理存儲器中數據的訪問有所不同,在運行可執行程序時并不進行程序代碼和數據的從磁盤文件到頁文件的復制過程,而是在確定了程序的代碼及其數據的大小后,由系統直接將可執行程序的映像用作程序的保留地址空間區域。這樣的處理方式大大縮短了程序的啟動時間,并可減小頁文件的尺寸。

     

     

    上面提到的“數據是否在內存中”,我認為應該是判斷系統緩存中是否有需要的頁面。

    ==========================================================================================

    對內存的管理

      使用虛擬內存技術將能夠對內存進行管理。對當前內存狀態的動態信息可通過GlobalMemoryStatus()函數來獲取。GlobalMemoryStatus()的函數原型為:

     

    VOID GlobalMemoryStatus(LPMEMORYSTATUS lpBuffer);

     

      其參數lpBuffer為一個指向內存狀態結構MEMORYSTATUS的指針,而且要預先對該結構對象的數據成員進行初始化。MEMORYSTATUS結構定義如下:

     

    typedef struct _MEMORYSTATUS {

     DWORD dwLength; // MEMORYSTATUS結構大小

     DWORD dwMemoryLoad; // 已使用內存所占的百分比

     DWORD dwTotalPhys; // 物理存儲器的總字節數

     DWORD dwAvailPhys; // 空閑物理存儲器的字節數

     DWORD dwTotalPageFile; // 頁文件包含的最大字節數

     DWORD dwAvailPageFile; // 頁文件可用字節數

     DWORD dwTotalVirtual; // 用戶模式分區大小

     DWORD dwAvailVirtual; // 用戶模式分區中空閑內存大小

    } MEMORYSTATUS, *LPMEMORYSTATUS;

    下面這段代碼通過設置一個定時器而每隔5秒更新一次當前系統對內存的使用情況:

    // 設置定時器

    SetTimer(0, 5000, NULL);

    ……

    void CSample22Dlg::OnTimer(UINT nIDEvent)

    {

     // 獲取當前內存使用狀態

     MEMORYSTATUS mst;

     GlobalMemoryStatus(&mst);

     // 已使用內存所占的百分比

     m_dwMemoryLoad = mst.dwMemoryLoad;

     // 物理存儲器的總字節數

     m_dwAvailPhys = mst.dwAvailPhys / 1024;

     // 空閑物理存儲器的字節數

     m_dwAvailPageFile = mst.dwAvailPageFile / 1024;

     // 頁文件包含的最大字節數

     m_dwAvailVirtual = mst.dwAvailVirtual / 1024;

     // 頁文件可用字節數

     m_dwTotalPageFile = mst.dwTotalPageFile / 1024;

     // 用戶模式分區大小

     m_dwTotalPhys = mst.dwTotalPhys / 1024;

     // 用戶模式分區中空閑內存大小

     m_dwTotalVirtual = mst.dwTotalVirtual / 1024;

     // 更新顯示

     UpdateData(FALSE);

     CDialog::OnTimer(nIDEvent);

    }

     

      對內存的管理除了對當前內存的使用狀態信息進行獲取外,還經常需要獲取有關進程的虛擬地址空間的狀態信息。可由VirtualQuery()函數來進行查詢,其原型聲明如下:

     

    DWORD VirtualQuery(

     LPCVOID lpAddress, // 內存地址

     PMEMORY_BASIC_INFORMATION lpBuffer, // 指向內存信息結構的指針

     DWORD dwLength // 內存的大小

    );

     

      其中lpAddress參數為要查詢的虛擬內存地址,該值將被調整到最近的頁邊界處。當前計算機的頁面大小可通過GetSystemInfo()函數獲取,該函數需要一個指向SYSTEM_INFO結構的指針作為參數,獲取到的系統信息將填充在該數據結構對象中。下面這段代碼通過對GetSystemInfo()的調用而獲取了當前的系統信息:

     

    // 得到當前系統信息

    GetSystemInfo(&m_sin);

    // 位屏蔽,指明哪個CPU是活動的

    m_dwActiveProcessorMask = m_sin.dwActiveProcessorMask;

    // 保留的地址空間區域的分配粒度

    m_dwAllocationGranularity = m_sin.dwAllocationGranularity;

    // 進程的可用地址空間的最小內存地址

    m_dwMaxApplicationAddress = (DWORD)m_sin.lpMaximumApplicationAddress;

    // 進程的可用地址空間的最大內存地址

    m_dwMinApplicationAddress = (DWORD)m_sin.lpMinimumApplicationAddress;

    // 計算機中CPU的數目

    m_dwNumberOfProcessors = m_sin.dwNumberOfProcessors;

    // 頁面大小

    m_dwPageSize = m_sin.dwPageSize;

    // 處理器類型

    m_dwProcessorType = m_sin.dwProcessorType;

    //進一步細分處理器級別

    m_wProcessorLevel = m_sin.wProcessorLevel;

    // 系統處理器的結構

    m_wProcessorArchitecture = m_sin.wProcessorArchitecture;

    // 更新顯示

    UpdateData(FALSE);

    VirtualQuery()的第二個參數lpBuffer為一個指向MEMORY_BASIC_INFORMATION結構的指針。VirtualQuery()如成功執行,該結構對象中將保存查詢到的虛擬地址空間狀態信息。MEMORY_BASIC_INFORMATION結構的定義為:

    typedef struct _MEMORY_BASIC_INFORMATION {

     PVOID BaseAddress; // 保留區域的基地址

     PVOID AllocationBase; // 分配的基地址

     DWORD AllocationProtect; // 初次保留時所設置的保護屬性

     DWORD RegionSize; // 區域大小

     DWORD State; // 狀態(提交、保留或空閑)

     DWORD Protect; // 當前訪問保護屬性

     DWORD Type; // 頁面類型

    } MEMORY_BASIC_INFORMATION; 

     

      通過VirtualQuery()函數對由lpAddressdwLength參數指定的虛擬地址空間區域的查詢而獲取得到的相關狀態信息:

     

    // 更新顯示

    UpdateData(TRUE);

    // 虛擬地址空間狀態結構

    MEMORY_BASIC_INFORMATION mbi;

    // 查詢指定虛擬地址空間的狀態信息

    VirtualQuery((LPCVOID)m_dwAddress, &mbi, 1024);

    // 保留區域的基地址

    m_dwBaseAddress = (DWORD)mbi.BaseAddress;

    // 分配的基地址

    m_dwAllocateBase = (DWORD)mbi.AllocationBase;

    // 初次保留時所設置的保護屬性

    m_dwAllocateProtect = mbi.AllocationProtect;

    // 區域大小

    m_dwRegionSize = mbi.RegionSize;

    // 狀態(提交、保留或空閑)

    m_dwState = mbi.State;

    // 當前訪問保護屬性

    m_dwProtect = mbi.Protect;

    // 頁面類型

    m_dwType = mbi.Type;

    // 更新顯示

    UpdateData(FALSE);

     

      小結

     

      本文主要對內存管理中的虛擬內存技術的基本原理、使用方法和對內存的管理等進行了介紹。通過本文將能夠掌握虛擬內存的一般使用方法,與之相關的內存管理技術還包括內存文件映射和堆管理等技術,讀者可參閱相關文章。這幾種內存管理技術同屬Windows編程中的高級技術,在應用程序中適當使用將有助于程序性能的提高。本文所述程序在Windows 2000 Professional下由Microsoft Viusual C++ 6.0編譯通過。

    進程的虛擬地址空間

    每個進程都被賦予它自己的虛擬地址空間。對于3 2位進程來說,這個地址空間是4 G B,因為3 2位指針可以擁有從0 x 0 0 0 0 0 0 0 00 x F F F F F F F F之間的任何一個值。這使得一個指針能夠擁有4 294 967 296個值中的一個值,它覆蓋了一個進程的4 G B虛擬空間的范圍。對于6 4位進程來說,這個地址空間是1 6 E B1 01 8字節),因為6 4位指針可以擁有從0 x 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 x F F F F F F F F F F F F F F F F之間的任何值。這使得一個指針可以擁有18 446 744 073 709 551 616個值中的一個值,它覆蓋了一個進程的1 6 E B虛擬空間的范圍。這是相當大的一個范圍。

    由于每個進程可以接收它自己的私有的地址空間,因此當進程中的一個線程正在運行時,該線程可以訪問只屬于它的進程的內存。屬于所有其他進程的內存則隱藏著,并且不能被正在運行的線程訪問。

    注意在Windows 2000中,屬于操作系統本身的內存也是隱藏的,正在運行的線程無法訪問。這意味著線程常常不能訪問操作系統的數據。Windows 98中,屬于操作系統的內存是不隱藏的,正在運行的線程可以訪問。因此,正在運行的線程常常可以訪問操作系統的數據,也可以破壞操作系統(從而有可能導致操作系統崩潰)。在Windows 98中,一個進程的線程不可能訪問屬于另一個進程的內存。

    前面說過,每個進程有它自己的私有地址空間。進程A可能有一個存放在它的地址空間中的數據結構,地址是0 x 1 2 3 4 5 6 7 8而進程B則有一個完全不同的數據結構存放在它的地址空間中,地址是0 x 1 2 3 4 5 6 7 8。當進程A中運行的線程訪問地址為0 x 1 2 3 4 5 6 7 8的內存時,這些線程訪問的是進程A的數據結構。當進程B中運行的線程訪問地址為0 x 1 2 3 4 5 6 7 8的內存時,這些線程訪問的是進程B的數據結構。進程A中運行的線程不能訪問進程B的地址空間中的數據結構。反之亦然。

    當你因為擁有如此大的地址空間可以用于應用程序而興高采烈之前,記住,這是個虛擬地址空間,不是物理地址空間。該地址空間只是內存地址的一個范圍。在你能夠成功地訪問數據而不會出現違規訪問之前,必須賦予物理存儲器或者將物理存儲器映射到各個部分的地址空間。本章后面將要具體介紹這是如何操作的。

    虛擬地址空間如何分區

    每個進程的虛擬地址空間都要劃分成各個分區。地址空間的分區是根據操作系統的基本實現方法來進行的。不同的Wi n d o w s內核,其分區也略有不同。表 1顯示了每種平臺是如何對進程的地址空間進行分區的。

    1 進程的地址空間如何分區

    分區

    32Windows 2000(x86Alpha處理器)

    32Windows 2000(x86w/3GB用戶方式)

    64Windows 2000(AlphaIA-64處理器)

    Windows 98

    N U L L指針分配的分區

    0 x 0 0 0 0 0 0 0 0  ——0x 0 0 0 0 F F F F

    0 x 0 0 0 0 0 0 0 0 0 x 0 0 0 0 F F F F

    0x00000000 00000000 0x00000000 0000FFFF

    0 x 0 0 0 0 0 0 0 0 0 x 0 0 0 0 0 F F F

    DOS/16Windows應用程序兼容分區

    0 x 0 0 0 0 0 1 0 0 0 0 x 0 0 3 F F F F F

    用戶方式

    0 x 0 0 0 1 0 0 0 0—— 0 x 7 F F E F F F F<將近2G>

    0 x 0 0 0 1 0 0 0 0 0 x B F F E F F F F F

    0x00000000 00010000 0x000003FF FFFEFFFF

    0 x 0 0 4 0 0 0 0 0 0 x 7 F F F F F F F

    64-KB禁止進入分區

    0 x 7 F F F 0 0 0 0——0x7FFF FFFF

    0 x B F F F 0 0 0 0——0 x B F F F F F F F

    0 x 0 0 0 0 0 3 F F F F F F 0 0 0 0——0 x 0 0 0 0 0 3 F F F F F F F F F F

    共享內存映射

    0 x 8 0 0 0 0 0 0 0

    文件(MMF)內核方式

    0 x 8 0 0 0 0 0 0 0 —— 0 x F F F F F F F F<2G>

    0 x C 0 0 0 0 0 0 0 0 x F F F F F F F F

    0x00000400 00000000 0xFFFFFFFFF FFFFFFF

    0 x B F F F F F F F 0 x C 0 0 0 0 0 0 0 0 x F F F F F F F F

    1. NULL指針分區是NULL指針的地址范圍。
        
    對這個區域的讀寫企圖都將引發訪問違規。 
    2. DOS/WIN16
    分區是98中專門用于16位的
        DOS
    windows程序運行的空間,所有的16
        
    位程序將共享這個4M的空間。Win2000中不
        
    存在這個分區,16位程序也會擁有自己獨立的虛擬地址空間。有的文章中稱win2000中不能運行16位程序,是不確切的。 
    3.
    用戶分區是進程的私有領域,Win2000中,程序的可執行代碼和其它用戶模塊均加載在這里,內存映射文件也會加載在這里。Win98中的系統共享DLL和內存映射文件則加載在共享分區中。 
    4.
    禁止訪問分區只有在win2000中有。這個分區是用戶分區和內核分區之間的一個隔離帶,目的是為了防止用戶程序違規訪問內核分區。 
    5. MMF
    分區只有win98中有,所有的內存映射文件和系統共享DLL將加載在這個地址。而2000中則將其加載到用戶分區。 
    6. 
    內核方式分區對用戶的程序來說是禁止訪問的,操作系統的代碼在此。內核對象也駐留在此。
    另外要說明的是,win98中對于內核分區本也應該提供保護的,但遺憾的是并沒有做到,因而98中程序可以訪問內核分區的地址空間。
    對于用戶分區,又可以細分成若干區域。(這些區域具體會在第四階段詳細剖析。因為這部分內容牽扯到PE文件結構,只有學習并理解了PE文件結構后,才能理解這部分內容,為了便于后面的講解,在此講這部分區域先大致分為4塊:)

    3 2Windows 2000的內核與6 4Windows 2000的內核擁有大體相同的分區,差別在于分區的大小和位置有所不同。另一方面,可以看到Windows 98下的分區有著很大的不同。下面讓我們看一下系統是如何使用每一個分區的。

    NULL指針分配的分區適用于Windows 2000Windows 98

    進程地址空間的這個分區的設置是為了幫助程序員掌握N U L L指針的分配情況。如果你的進程中的線程試圖讀取該分區的地址空間的數據,或者將數據寫入該分區的地址空間,那么C P U就會引發一個訪問違規。保護這個分區是極其有用的,它可以幫助你發現N U L L指針的分配情況。

    C / C + +程序中常常不進行嚴格的錯誤檢查。例如,下面這個代碼就沒有進行任何錯誤檢查:

    int* pnSomeInteger = (int*) malloc(sizeof(int));
    *pnSomeInteger = 5;

    如果m a l l o c不能找到足夠的內存來滿足需要,它就返回N U L L。但是,該代碼并不檢查這種可能性,它認為地址的分配已經取得成功,并且開始訪問0 x 0 0 0 0 0 0 0 0地址的內存。由于這個分區的地址空間是禁止進入的,因此就會發生內存訪問違規現象,同時該進程將終止運行。這個特性有助于編程員發現應用程序中的錯誤。

    用戶方式分區適用于Windows 2000Windows 98

    這個分區是進程的私有(非共享)地址空間所在的地方。一個進程不能讀取、寫入、或者以任何方式訪問駐留在該分區中的另一個進程的數據。對于所有應用程序來說,該分區是維護進程的大部分數據的地方。由于每個進程可以得到它自己的私有的、非共享分區,以便存放它的數據,因此,應用程序不太可能被其他應用程序所破壞,這使得整個系統更加健壯。

    Windows 2000中,所有的. e x eD L L模塊均加載這個分區。每個進程可以將這些D L L加載到該分區的不同地址中(不過這種可能性很小)。系統還可以在這個分區中映射該進程可以訪問的所有內存映射文件

    共享的MMF分區僅適用于Windows 98

    這個1 G B分區是系統用來存放所有3 2位進程共享數據的地方。例如,系統的動態鏈接庫K e r n e l 3 2 . d l lA d v A P I 3 2 . d l lU s e r 3 2 . d l lG D I 3 2 . d l l等,全部存放在這個地址空間分區中,因此,所有3 2位進程都能很容易同時訪問它們。系統還為每個進程將D L L加載相同的內存地址。此外,系統將所有內存映射文件映射到這個分區中。

    物理存儲器與頁文件

    在較老的操作系統中,物理存儲器被視為計算機擁有的R A M的容量。換句話說,如果計算機擁有1 6 M BR A M,那么加載和運行的應用程序最多可以使用1 6 M BR A M。今天的操作系統能夠使得磁盤空間看上去就像內存一樣。磁盤上的文件通常稱為頁文件,它包含了可供所有進程使用的虛擬內存

    當然,若要使虛擬內存能夠運行,需要得到C P U本身的大量幫助。當一個線程試圖訪問一個字節的內存時, C P U必須知道這個字節是在R A M中還是在磁盤上。

    從應用程序的角度來看,頁文件透明地增加了應用程序能夠使用的R A M(即內存)的數量。如果計算機擁有6 4 M BR A M,同時在硬盤上有一個100 MB的頁文件,那么運行的應用程序就認為計算機總共擁有1 6 4 M BR A M

    實際上并不擁有1 6 4 M BR A M。相反,操作系統與C P U相協調,共同將R A M的各個部分保存到頁文件中,當運行的應用程序需要時,再將頁文件的各個部分重新加載到R A M。由于頁文件增加了應用程序可以使用的R A M的容量,因此頁文件的使用是視情況而定的。如果沒有頁文件,那么系統就認為只有較少的R A M可供應用程序使用。但是,我們鼓勵用戶使用頁文件,這樣他們就能夠運行更多的應用程序,并且這些應用程序能夠對更大的數據集進行操作。最好將物理存儲器視為存儲在磁盤驅動器(通常是硬盤驅動器)上的頁文件中的數據。這樣,當一個應用程序通過調用Vi r t u a l A l l o c函數,將物理存儲器提交給地址空間的一個區域時,地址空間實際上是從硬盤上的一個文件中進行分配的。系統的頁文件的大小是確定有多少物理存儲器可供應用程序使用時應該考慮的最重要的因素, R A M的容量則影響非常小。

    第一種情況中,線程試圖訪問的數據是在R A M中。在這種情況下, C P U將數據的虛擬內存地址映射到內存的物理地址中,然后執行需要的訪問。線程試圖訪問的數據不在R A M中,而是存放在頁文件中的某個地方。這時,試圖訪問就稱為頁面失效, C P U將把試圖進行的訪問通知操作系統。這時操作系統就尋找R A M中的一個內存空頁。如果找不到空頁,系統必須釋放一個空頁。如果一個頁面尚未被修改,系統就可以釋放該頁面。但是,如果系統需要釋放一個已經修改的頁面,那么它必須首先將該頁面從R A M拷貝到頁交換文件中,然后系統進入該頁文件,找出需要訪問的數據塊,并將數據加載到空閑的內存頁面。然后,操作系統更新它的用于指明數據的虛擬內存地址現在已經映射到R A M中的相應的物理存儲器地址中的表。這時C P U重新運行生成初始頁面失效的指令,但是這次C P U能夠將虛擬內存地址映射到一個物理R A M地址,并訪問該數據塊。

    當閱讀了上一節后,你必定會認為,如果同時運行許多文件的話,頁文件就可能變得非常大,而且你會認為,每當你運行一個程序時,系統必須為進程的代碼和數據保留地址空間的一些區域,將物理存儲器提交給這些區域,然后將代碼和數據從硬盤上的程序文件拷貝到頁文件中已提交的物理存儲器中。

    實際上系統并不進行上面所說的這些操作。如果它進行這些操作的話,就要花費很長的時間來加載程序并啟動它運行。相反,當啟動一個應用程序的時候,系統將打開該應用程序的. e x e文件,確定該應用程序的代碼和數據的大小。然后系統要保留一個地址空間的區域,并指明與該區域相關聯的物理存儲器是在. e x e文件本身中即系統并不是從頁文件中分配地址空間,而是將. e x e文件的實際內容即映像用作程序的保留地址空間區域。當然,這使應用程序的加載非常迅速,并使頁文件能夠保持得非常小

    一、開始之前,讓我們來了解一下Windows中內存管理的一些知識:

     

    1. 機器的物理內存由兩部分組成。一部分為機器的主存RAM,也就是我們內存條的大小;另一部分為虛擬內存,它就在機器的硬盤上,以頁文件的形式存在。

     

    2. 每個進程都有自己的虛擬地址空間,對于具有32位尋址能力的機器來說,這個虛擬空間的大小為4GB。現在我們使用的機器就是4GB

     

    3. 進程的4GB虛擬地址空間又可以分成幾個部分,其中進程真正私有的空間少于2GB(這段地址空間被稱作“用戶方式分區”),其余的2GB多空間都是給操作系統的,且這部分空間被所有的進程共享。(參考Windows核心編程Chapter 13

     

    4. 為進程“分配內存”,這個概念可以細化:“保留段地址空間”,“提交一段內存空間”,“將內存空間映射到主存”。在程序中我們通常所訪問的地址都必須是進程地址空間中被保留和提交的那段地址空間。

     

    4.1 “保留段地址空間”:即從進程4GB地址空間中保留段地址空間,這個過程通過VirtualAlloc函數完成,并把分配類型參數設置為MEM_RESERVE。這段空間的起始地址必須是系統分配粒度的整數,大小必須是系統頁面大小的整數

     

    4.2 “提交一段內存空間”:即為進程已保留的地址空間映射機器的物理內存,這里要特別注意,所謂物理內存一般并不是機器的主存,而只是機器的虛擬內存。這個過程同樣又VirtualAlloc完成,只是把分配類型參數設置為MEM_COMMIT。這段空間的起始地址和大小都必須是頁面大小的整數。這樣進程的對應被提交的區域就被映射到機器的虛擬內存上。

     

    4.3 “將內存空間映射到主存”:這點很重要,操作系統總是只有在進程提交的頁面被訪問時才將相應的頁面加載到主存中,同時修改進程對應頁面的地址空間映射。這時,進程的地址空間中的對應區域才和機器上的主存對應起來。

     

    Virtual Size

     

          該指標記錄了當前進程申請成功的其虛擬地址空間的總的空間大小,包括DLL/EXE占用的地址和通過VirtualAlloc API ReserveMemory Space數量。請注意,該指標包括保留的地址空間。

     

    Private Bytes

     

           該指標記錄了進程用戶方式分區地址空間中已提交的總的空間大小。無論是直接調用API申請的內存,被Heap Manager申請的內存,或者是CLR managed heap,都算在里面。

     

    Working Set

     

           該指標記錄了所有映射到進程虛擬地址空間的機器主存的大小,它不僅僅是用戶方式分區部分的映射,而是整個進程地址空間的映射。即它同時包括內核方式分區中映射到機器主存的部分。由4.3可知,在用戶方式分區部分只有在進程提交的頁面被訪問時才將相應的頁面加載到主存中。而對于該部分的大小總是系統頁面大小的整數

     

           這里有一個問題,隨著進程的不斷運行,進程被訪問的頁面將可能不斷增加,這是否意味著“Working Set”的大小會不斷的累加呢?顯然不是。在程序運行過程中影響“Working Set”的因素包括:(1) 機器可用主存的大小 (2) 進程本身“Working Set”的大小范圍。當機器的可用主存小于一定值時,系統會釋放一些老的最近沒有被訪問的頁面,把這些頁面通過交換文件交換到機器的虛擬內存中;當Working Set的大小大于該進程所設置的最大值時,同樣會把一些老的頁面交換到機器的虛擬內存中。當這些頁面下次再被訪問時,它們才加載到主存。

     

           由上可知,Working Set“一定比”Private Bytes“小,因為它只是”Private Bytes“對應的地址空間中被加載到主存的那部分

     

    Page Faults”

     

           該指標和Working Set密切相關,當進程訪問某個頁面,而這個頁面卻不在主存中時,就要發生一次Page Fault“,即進程訪問非”Working Set“中的頁面時,發生一次”Page Fault“,同時系統將對應頁面加載到主存中。

     

           接下來的三個指標是對Working Set“的細化:

     

    WS Private“

     

           該指標記錄了進程Working Set“中被該進程所獨享的空間大小。

     

    "WS Shareable"

     

           該指標記錄了進程Working Set“中能與別的進程共享的空間大小

     

    WS Shared“

     

           該指標記錄了進程Working Set“中已經與別的進程共享的空間大小

     

    WS Shareable“和”WS Shared“兩個指標一看令人感到疑惑,因為既然”Working Set“屬于”Private Bytes“中的一部分,而”Private Bytes“是進程私有的,為什么會有”WS Shareable“和”WS Shared“這兩項呢?

     

           認真一想,其實很容易理解,比如兩個進程都需要同一個DLL的支持,所以在進程運行過程中,這個DLL被映射到了兩個進程的地址空間中,如果這個DLL的大小為4K,在兩個進程中都要提交4K的虛擬地址空間來映射這個DLL。當第一個進程訪問了這個DLL時,這個DLL被加載到機器主存中,這時,第二個進程也要訪問該DLL,這時,系統就不會再加載一遍該DLL了,因為這個DLL已經在主存中了。當然上面所說的訪問僅僅是讀取的操作,如果這時候某個進程要修改DLL對應這段地址中的某個單元時,這時,系統必須為第二個進程分配另外的新頁面,并把要修改位置對應的頁面拷貝的這個新頁面,同時,第二個進程中的這個DLL被映射到這個新頁面上。

     

           上面的分析中,DLL對應的4K的內存在第一個進程中便是WS Shareable“。另外,內核方式分區中的所有代碼都是被所有進程共享的,只要一個進程訪問了這些頁面,則在所有的進程的Working Set“中都能體現。

     

    三、下面我們來討論一下這些內存指標與進程內存消耗之間的關系

     

           在計算機更新換代不斷加速的今天,我們往往很少關注程序對內存的消耗,除非程序的內存消耗超出了我們的忍受范圍——大量的泄漏、運行速度下降等。

     

           那么,當我們在測進程的內存使用量時,到底應該使用哪個指標能更好的反應程序的內存消耗呢?由于Windows自帶的Task Manager中的Memory Usage“所對應的指標就是”Working Set,所以大部分人認為該指標能夠很好的反應進程的內存使用量。

     

    在得出結論之前,讓我們來分析一下以上的這些指標:

     

    就從Working Set“開始吧。

     

    Working Set“:

     

           進程中被加載到機器主存的所有頁面大小的。它可細分為WS Shareable“和”WS Shared“。進程訪問頁面不再Working Set“中時,會發生一次”Page Fault“且同時發生一次主存與虛擬內存之間的數據交換。綜上所述,我們可以得出結論:

     

    (a)Working Set“不是進程內存消耗的全部;

     

    (b)所有進程Working Set“的和也不等有機器主存總的消耗量,因為存在”Working Shareable“與別的進程共享;

     

    (c)Working Set“太大會影響機器的運行速度,因為”Working Set“太大會導致機器的可用主存太少,從而導致將進程的老頁面釋放到虛擬內存,同時,進程”Working Set“中的頁面減少后,使進程發生”Page Fault“的頻率更高。因為在主存與虛擬內存之間交換數據需要時間,所以機器的運行速度要減慢。

     

    (d)Working Set“由于數據交換的存在,該指標是動態的,在測量的過程中會不斷變化。(變化的最小單位為4K

     

           所以Working Set“指標強調的是進程對機器主存的消耗,不是進程內存的全部信息。

     

    "Private Bytes"

     

           該指標包含所有為進程提交的內存,包括機器主存和虛擬內存,可以認為它是進程對物理內存消耗且該指標相對來說更加穩定。在程序產生內存泄漏時,該值一定是不斷上漲的。

     

           綜上所述,個人更傾向于使用Private Bytes“來定量進程的內存消耗和分析進程的內存泄漏。

    posted on 2008-11-20 11:15 so true 閱讀(23647) 評論(7)  編輯  收藏 所屬分類: Others

    評論

    # re: 進程的虛擬地址空間  回復  更多評論   

    這篇文章是我本人在互聯網上找到的關于進程地址空間說的最好了了,歡迎來頂作者!這樣的文章真的太珍貴!!!
    2011-03-31 16:13 | yiruirui

    # re: 進程的虛擬地址空間  回復  更多評論   

    @yiruirui
    好文章,收藏了,好好學習一下.
    2011-11-01 08:45 | scient

    # re: 進程的虛擬地址空間  回復  更多評論   

    非常好的文章!!!
    2012-02-14 15:52 | shory

    # re: 進程的虛擬地址空間  回復  更多評論   

    好文章,感謝樓主~
    2012-04-02 09:47 | adaByron

    # re: 進程的虛擬地址空間  回復  更多評論   

    神,你是怎么煉成的
    2012-05-07 08:49 | readily

    # re: 進程的虛擬地址空間[未登錄]  回復  更多評論   

    exe通過內存文件映射成為進程虛擬地址空間。那么exe文件中的 導出表 導入表 等等section是否也映射到虛擬地址空間里面了呢?還是只映射了
    -------------
    程序代碼和數據

    其他庫文件

    內核虛擬存儲器
    --------------------
    這些東西?
    文件映射都是進程地址空間保留了文件中的所有內容。
    那么進程的地址空間中是否也保留了exe中的所有內容?
    2013-04-10 20:19 | kenny

    # re: 進程的虛擬地址空間[未登錄]  回復  更多評論   

    寫的真是太透徹了
    2013-10-28 22:30 | ccc
    主站蜘蛛池模板: 最近2019中文字幕mv免费看| 午夜免费1000部| 国产成人久久AV免费| 亚洲w码欧洲s码免费| 色www永久免费视频| 国产亚洲精品免费视频播放| 亚洲免费精彩视频在线观看| 国产精品亚洲精品青青青| 在线观看国产一区亚洲bd| 国产午夜无码片免费| 亚洲人成免费网站| 日本高清免费不卡在线| 亚洲女久久久噜噜噜熟女| 亚洲天堂电影在线观看| 国产亚洲精品91| 四虎影视在线影院在线观看免费视频 | 久久亚洲精品无码播放| 久久精品国产亚洲AV无码娇色| 99亚偷拍自图区亚洲| 国产人成网在线播放VA免费| 99视频全部免费精品全部四虎| 国产一区二区三区在线免费观看| 亚洲精品国产美女久久久| 2019亚洲午夜无码天堂| 一个人免费观看www视频| 免费H网站在线观看的| 久久亚洲2019中文字幕| 亚洲乱码卡三乱码新区| 久久成人永久免费播放| 国产99视频精品免费观看7| 亚洲精品一级无码中文字幕| 亚洲精品一卡2卡3卡三卡四卡| 看免费毛片天天看| 久久A级毛片免费观看| 亚洲av无码国产精品色在线看不卡| 亚洲丝袜美腿视频| 免费国产污网站在线观看不要卡| 精品一区二区三区无码免费视频| 亚洲av无码专区在线观看素人| 亚洲理论片在线中文字幕| 国产精品免费观看视频|