• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            posts - 126,  comments - 73,  trackbacks - 0

            I/O完成端口背后的理論是同時運行的線程數(shù)必須有個上界;也就是,500個并發(fā)的客戶端請求不必要500個線程存在。那么,合適的并發(fā)線程數(shù)是多少呢?你會意識到,如果一個機器有兩個CPU,那么在此基礎(chǔ)上多余兩個以上的線程實在是沒有意義。因為,當有超過CPU數(shù)量的線程數(shù)時,系統(tǒng)不得不耗費時間來進行線程之間的切換,這會浪費寶貴的CPU時鐘周期。
            為每個客戶端創(chuàng)建一個線程還有一個不足,就是創(chuàng)建一個線程雖然比創(chuàng)建一個進程廉價,但它遠遠不是免費的。如果服務(wù)器應(yīng)用程序在啟動時創(chuàng)建一個線程池,并且池子中的線程留駐在程序的整個生命期內(nèi),那么服務(wù)器的性能會得到提高。I/O完成端口在設(shè)計上就是使用了線程池。
            I/O完成端口可能是最復(fù)雜的核心對象。要創(chuàng)建一個完成端口你可以使用CreateIoCompletionPort函數(shù)。

            HANDLE CreateIoCompletionPort(
            ?? HANDLE??? hfile,
            ?? HANDLE??? hExistingCompPort,
            ?? ULONG_PTR CompKey,
            ?? DWORD???? dwNumberOfConcurrentThreads);
            ???
            或者封裝:
            HANDLE CreateNewCompletionPort(DWORD dwNumberOfConcurrentThreads) {

            ?? return(CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0,
            ????? dwNumberOfConcurrentThreads));
            }

            這個函數(shù)根據(jù)傳入的參數(shù)的不同,可以完成兩個完全不同的功能。一是可以創(chuàng)建一個完成端口,另一個是可以將一個設(shè)備與完成端口關(guān)聯(lián)起來。例如要產(chǎn)生一個完成端口,看起來可以這樣調(diào)用函數(shù):
            hCompPort = CreateIoCompletionPort(
            ???INVALID_HANDLE_VALUE,
            ???NULL,
            ???0,
            ???0);

            FileHandle —— 文件或設(shè)備的句柄。在產(chǎn)生完成端口時,這個參數(shù)應(yīng)設(shè)置為INVALID_HANDLE_VALUE,于是產(chǎn)生一個沒有和任何句柄有關(guān)系的port。
            ExistingCompletionPort —— 在產(chǎn)生完成端口時此參數(shù)設(shè)定為NULL,產(chǎn)生一個新的port。如果此參數(shù)指定為一個已存在的完成端口的句柄,那么在上一個參數(shù)FileHandle中指定的句柄就會被加到此port上,而不會產(chǎn)生新的port。
            CompletionKey —— 用戶自定義的一個值,將被交給提供服務(wù)的線程。此值和FileHandle有關(guān)聯(lián)。
            NumberOfConcurrentThreads —— 與此完成端口有關(guān)聯(lián)的線程個數(shù)。如果設(shè)定為0則為CPU的個數(shù)。
            NumberOfConcurrentThreads這個參數(shù)是告訴完成端口可同時運行的最大線程數(shù)。如果傳0,則默認值為CPU的個數(shù)。在大多數(shù)情況下為了避免不必要的線程切換你可以傳0。有時你也許會想要增加這個值,那就是當來自客戶端的請求需要一個長時間的計算甚至發(fā)生阻塞時,但通常不鼓勵增加這個值。你可以嘗試往這個參數(shù)傳不同的值來測試比較服務(wù)器的性能。

            關(guān)聯(lián)一個設(shè)備到完成端口
            當你創(chuàng)建完成端口時,系統(tǒng)內(nèi)核會創(chuàng)建5個不同的數(shù)據(jù)結(jié)構(gòu).

            第一個數(shù)據(jù)結(jié)構(gòu)是設(shè)備列表,指明了被關(guān)聯(lián)到完成端口上的設(shè)備。將設(shè)備關(guān)聯(lián)到完成端口上,你同樣是調(diào)用CreateIoCompletionPort函數(shù)。看起來可以這樣調(diào)用函數(shù):
            HANDLE h = CreateIoCompletionPort(hDevice, hCompPort, dwCompKey, 0);

            或者封裝:
            BOOL AssociateDeviceWithCompletionPort(
            ?? HANDLE hCompPort, HANDLE hDevice, DWORD dwCompKey) {

            ?? HANDLE h = CreateIoCompletionPort(hDevice, hCompPort, dwCompKey, 0);
            ?? return(h == hCompPort);
            }
            這樣就添加一個記錄(entry)到一個已經(jīng)存在的完成端口設(shè)備列表上。你在第一個參數(shù)上傳遞一個已經(jīng)存在的完成端口句柄(在第一次調(diào)CreateIoCompletionPort時得到),在第二個參數(shù)上傳遞設(shè)備句柄(這個設(shè)備可以是file,socket,
            mailslot,pipe等等),在第三個參數(shù)上傳遞一個完成鍵(由用戶定義的值,操作系統(tǒng)不關(guān)心你傳遞的是什么值)。每當你關(guān)聯(lián)一個設(shè)備到完成端口上時,系統(tǒng)就添加這些信息到完成端口設(shè)備列表上。
            注意:CreateIoCompletionPort很復(fù)雜,推薦把它看成兩個函數(shù)來使用。下面有個例子把創(chuàng)建端口和關(guān)聯(lián)放在一次調(diào)用中完成,但是不推薦這樣使用。下面的代碼在打開一個文件并在同一時間創(chuàng)建一個新的完成端口,同時關(guān)聯(lián)該設(shè)備與完成端口。所有這個文件I/O請求都將在完成時使用CK_FILE這個完成鍵,完成端口將允許兩個線程來并發(fā)執(zhí)行。
            #define CK_FILE?? 1
            HANDLE hfile = CreateFile(...);
            HANDLE hCompPort = CreateIoCompletionPort(hfile, NULL, CK_FILE, 2);


            第二個數(shù)據(jù)結(jié)構(gòu)是一I/O完成隊列。當針對一個設(shè)備的異步I/O請求完成時,系統(tǒng)就檢查看這個設(shè)備在之前是否被關(guān)聯(lián)到一個完成端口過,如果是,系統(tǒng)就將這個已經(jīng)完成的I/O請求作為一條記錄,添加到對應(yīng)的完成端口的I/O完成隊列末尾。在隊列中的每條記錄都指明了4個值:已傳輸?shù)淖止?jié)數(shù);完成鍵(當設(shè)備被關(guān)聯(lián)到端口上時通過第三個參數(shù)設(shè)定的);一個指針,指向I/O請求的OVERLAPPED結(jié)構(gòu);和一個錯誤碼。
            注意:對一個設(shè)備發(fā)出一個I/O請求,但并不將它記錄到完成端口的完成隊列上是可以的。這不是常態(tài),但有時卻派得上用場。比如,當你通過socket發(fā)送數(shù)據(jù),但你并不在意這些數(shù)據(jù)是否被處理時。(或者有時候想用舊有的受激發(fā)的event對象機制來激發(fā),而不用I/O完成隊列)
            為了這么做,請你設(shè)定一個OVERLAPPED結(jié)構(gòu),內(nèi)含一個合法的手動重置(manual-reset)event對象,放在hEvent欄位。然后把該handle的最低位設(shè)置為1。雖然這聽起來象是一種黑客行為,但檔案中有交代。看起來象這樣:
            Overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
            Overlapped.hEvent = (HANDLE) ((DWORD_PTR) Overlapped.hEvent | 1);
            ReadFile(..., &Overlapped);

            同樣,不要忘記在關(guān)閉event句柄之前重置低位:
            CloseHandle((HANDLE) ((DWORD_PTR) Overlapped.hEvent & ~1));


            Architecting Around an I/O Completion Port
            當你的服務(wù)器應(yīng)用程序啟動時,它將通過調(diào)用像CreateNewCompletionPort這樣的函數(shù)來創(chuàng)建I/O完成端口。然后應(yīng)用程序創(chuàng)建一個線程池來處理client請求。現(xiàn)在你有個問題要問,“線程池中有多少線程?”。這是一個很難回答的問題,我將在“How Many Threads in the Pool?”這個小節(jié)中給出詳細的回答。目前,單憑經(jīng)驗的標準是本地機器上的CPU數(shù)再乘以2。比如,一個雙CPU的機器上,你就應(yīng)該在線程池中創(chuàng)建4個線程。
            池子里的所有線程都將執(zhí)行相同的線程函數(shù)。通常,這個線程函數(shù)執(zhí)行一些初始化工作,然后就進入一個循環(huán),直到服務(wù)進程被指示停止時才結(jié)束掉。在循環(huán)里,線程將自己休眠,等待關(guān)聯(lián)到完成端口上的設(shè)備的I/O請求完成。線程通過調(diào)用GetQueuedCompletionStatus函數(shù)進入等待。
            下面是函數(shù)原型:

            BOOL GetQueuedCompletionStatus(
            ?? HANDLE?????? hCompPort,
            ?? PDWORD?????? pdwNumBytes,
            ?? PULONG_PTR?? CompKey,
            ?? OVERLAPPED** ppOverlapped,
            ?? DWORD??????? dwMilliseconds);
            第一個參數(shù)hCompPort傳遞一個完成端口句柄,表明線程監(jiān)控的是哪一個完成端口。許多服務(wù)程序都是使用的單一完成端口,所有I/O請求的完成通知都在這一個端口上。基本上,GetQueuedCompletionStatus的工作就是將調(diào)用此函數(shù)的線程休眠,直到一條記錄出現(xiàn)在指定完成端口上的I/O完成隊列中,或者直到指定的超時時間到達(超時時間在dwMilliseconds參數(shù)中指定)。
            第三個數(shù)據(jù)結(jié)構(gòu)就是等待中的線程隊列(the waiting thread queue)。在線程池中的每一個調(diào)用了GetQueuedCompletionStatus的線程,其線程ID就被放置到等待中的線程隊列里,以使I/O完成端口核心對象能知道當前有哪些線程正等待著處理已經(jīng)完成了的I/O請求。當有一條記錄出現(xiàn)在端口的I/O完成隊列時,完成端口就喚醒一個在等待隊列中的線程。這個線程會得到組成這條完成記錄的一些信息:已傳輸?shù)淖止?jié)數(shù),完成鍵,和一個OVERLAPPED結(jié)構(gòu)的地址。這些信息通過傳遞給GetQueuedCompletionStatus函數(shù)的三個參數(shù)(pdwNumBytes、CompKey、ppOverlapped)返回到線程中。GetQueuedCompletionStatus函數(shù)的返回值有點復(fù)雜,下面的代碼演示了如何處理不同的返回值:
            DWORD dwNumBytes;
            ULONG_PTR CompKey;
            OVERLAPPED* pOverlapped;

            // hIOCP is initialized somewhere else in the program
            BOOL fOk = GetQueuedCompletionStatus(hIOCP,
            ?? &dwNumBytes, &CompKey, &pOverlapped, 1000);
            DWORD dwError = GetLastError();

            if (fOk) {
            ?? // Process a successfully completed I/O request
            } else {
            ?? if (pOverlapped != NULL) {
            ????? // Process a failed completed I/O request
            ????? // dwError contains the reason for failure
            ?? } else {
            ????? if (dwError == WAIT_TIMEOUT) {
            ???????? // Time-out while waiting for completed I/O entry
            ????? } else {
            ???????? // Bad call to GetQueuedCompletionStatus
            ???????? // dwError contains the reason for the bad call
            ????? }
            ?? }
            }
            如你所預(yù)料到的,I/O完成隊列是以隊列的方式移出記錄的,也就是先進先出FIFO。然而,你可能沒有料到的是,調(diào)用GetQueuedCompletionStatus函數(shù)進入等待的線程卻是以棧的方式來喚醒的,也就是LIFO,最后調(diào)用函數(shù)進入等待的那個線程被最先喚醒。這樣做的理由是為了進一步提高處理性能。例如,有四個線程在“等待線程隊列”中等待著。如果這個時候只有一條“已完成的I/O”記錄出現(xiàn)了,那么最后一個調(diào)用GetQueuedCompletionStatus的線程被喚醒來處理這條記錄。當這個線程處理完這條記錄時,此線程再次調(diào)用GetQueuedCompletionStatus函數(shù)進入等待線程隊列。那么,此時當有另一條I/O完成記錄出現(xiàn)時,剛才那個線程被繼續(xù)喚醒來處理這條新的記錄。(這樣的好處就是可以避免線程間的切換)
            只要I/O請求完成得夠遲緩,那么單個線程就足以處理他們。系統(tǒng)只需要保持喚醒同一個線程,并讓其他三個線程持續(xù)休眠。使用LIFO算法,線程就不必花時間將他們在內(nèi)存中的資源置換到磁盤中(例如棧空間),并從CPU的cache中替換出去。

            I/O完成端口如何管理線程池
            現(xiàn)在該討論為什么完成端口是如此有用了。首先,在你創(chuàng)建完成端口的時候,你通過CreateIoCompletionPort函數(shù)的最后一個參數(shù)指定可以并行運行的線程數(shù)量。我們說過,這個值通常設(shè)置為主機上的CPU數(shù)量。當已經(jīng)完成的I/O記錄進入隊列時,完成端口就喚醒等待中的線程。然而,完成端口只能喚醒你所指定的那么多個線程。于是,在雙CPU的機器中,如果有4個I/O請求完成,并有4個線程在等待,但完成端口卻只能喚醒2個線程,另2個線程保持休眠。每個得到喚醒在處理已完成的I/O記錄的線程,處理完成后,又重新調(diào)用GetQueuedCompletionStatus函數(shù)進入等待。因為線程的喚醒算法是后進先出,所以系統(tǒng)看到還有已完成的I/O記錄在排隊時,又喚醒剛才被喚醒過的線程。
            仔細思考這個過程,你似乎會覺得好象有些問題:既然完成端口僅允許指定數(shù)量的線程被喚醒,為什么還要產(chǎn)生多余的線程等待在線程池中呢?比如,在上一段中所敘述的那種情況,在一個雙CPU的主機上,創(chuàng)建一個完成端口,并告之只允許2個線程并行處理。但是創(chuàng)建線程池時我們創(chuàng)建了4個線程(按照2倍CPU的數(shù)量),看上去我們好象創(chuàng)建了2個多余的線程——他們永遠也不會被喚醒。(因為LIFO)
            但是不用擔心,完成端口是非常聰明的。當完成端口喚醒一個線程時,會將此線程的ID放入第四個數(shù)據(jù)結(jié)構(gòu)中——被釋放的線程列表。這可以讓完成端口記住哪些線程是被喚醒了的,并監(jiān)控這些線程的執(zhí)行。如果某個在“被釋放的線程列表”中的線程因為調(diào)用了某功能而被掛起(即產(chǎn)生阻塞)那么完成端口會偵測到這一情況,并將此線程的ID從“被釋放的線程列表”中移動到“暫停的線程列表”——即第五個數(shù)據(jù)結(jié)構(gòu)中。
            完成端口的目標就是要保證在“被釋放的線程列表”中的記錄條數(shù)保持為你在創(chuàng)建完成端口時所指定的并發(fā)線程數(shù)。因此,當一個被喚醒的線程因為某種原因而被掛起時,完成端口就會釋放掉另一個在等待中的線程,以次來維持“被釋放的線程列表”中的記錄數(shù)保持為你所指定的并發(fā)線程數(shù)。如果被掛起的線程重新被喚醒,它又會從“暫停的線程列表”中移動到“被釋放的線程列表”。這個時候需要注意的是加上剛才被另外喚醒的一個線程,此時在“被釋放的線程列表”中的記錄數(shù)會超過你所指定的并發(fā)線程數(shù)(看上去很奇怪,但卻是這樣)。完成端口意識到這一情況后將不會再喚醒其他線程,除非并行線程數(shù)又降到CPU數(shù)量之下。實際并行線程數(shù)將只在你指定的最大并行線程數(shù)之上停留很短的時間并且線程在循環(huán)到再次調(diào)用GetQueuedCompletionStatus時這種情況將快速的停止下來。
            注意:一旦線程調(diào)用GetQueuedCompletionStatus,那么這個線程就被“分配”到那指定的完成端口上去。系統(tǒng)會假定那些被“分配”的線程代表完成端口在工作。你可以終止線程和完成端口間的這種“分配”關(guān)系,通過以下三種方式:
            ??終止線程;
            ??讓線程調(diào)用GetQueuedCompletionStatus,并傳遞另一個完成端口的句柄,這樣就終止掉當前的“分配”關(guān)系,與另一完成端口產(chǎn)生新的“分配”關(guān)系。
            ??銷毀完成端口。

            在線程池中創(chuàng)建多少個線程?
            現(xiàn)在是討論線程池中需要多少個線程的好時機。考慮兩點:首先,當服務(wù)程序啟動時你應(yīng)該創(chuàng)建一個最小的線程集,這樣你就不用象每客戶端每線程模型那樣頻繁地創(chuàng)建和銷毀線程了。因為創(chuàng)建和銷毀線程都需要耗費CPU時間,所以你應(yīng)該盡可能的少做這些操作。其次,你應(yīng)該設(shè)置線程數(shù)的最大值,因為創(chuàng)建太多的線程會相應(yīng)耗費太多的系統(tǒng)資源。就算大多數(shù)資源能被交換出RAM,但你還是應(yīng)該最小限度地使用系統(tǒng)資源,不要浪費,最好不要產(chǎn)生paging file space。
            你也許應(yīng)該測試不同的線程數(shù)。大多數(shù)服務(wù)程序(包括MS的IIS)都使用啟發(fā)式(heuristic,實際就是枚舉選擇,通過可選擇的方法發(fā)現(xiàn)的幾種解決辦法中最合適的一種,在一個程序的連續(xù)階段被選擇出來以用于該程序的下一步的一種解決問題的技巧的應(yīng)用)算法來管理他們的線程池。我推薦你也使用同樣的方法。例如,你可以創(chuàng)建如下的變量來管理線程池。
            LONG g_nThreadsMin;??? // Minimum number of threads in pool
            LONG g_nThreadsMax;??? // Maximum number of threads in pool
            LONG g_nThreadsCrnt;?? // Current number of threads in pool
            LONG g_nThreadsBusy;?? // Number of busy threads in pool

            當你的程序啟動時,你可以創(chuàng)建g_nThreadsMin這么多個線程,都執(zhí)行相同的線程函數(shù)。線程函數(shù)看起來是這樣:
            DWORD WINAPI ThreadPoolFunc(PVOID pv) {

            ?? // Thread is entering pool
            ?? InterlockedIncrement(&g_nThreadsCrnt);
            ?? InterlockedIncrement(&g_nThreadsBusy);

            ?? for (BOOL fStayInPool = TRUE; fStayInPool;) {

            ????? // Thread stops executing and waits for something to do
            ????? InterlockedDecrement(&m_nThreadsBusy);
            ????? BOOL fOk = GetQueuedCompletionStatus(...);
            ????? DWORD dwIOError = GetLastError();

            ????? // Thread has something to do, so it's busy
            ????? int nThreadsBusy = InterlockedIncrement(&m_nThreadsBusy);

            ????? // Should we add another thread to the pool?
            ????? if (nThreadsBusy == m_nThreadsCrnt) {??? // All threads are busy
            ???????? if (nThreadsBusy < m_nThreadsMax) {?? // The pool isn't full
            ??????????? if (GetCPUUsage() < 75) {?? // CPU usage is below 75%

            ?????????????? // Add thread to pool
            ?????????????? CloseHandle(chBEGINTHREADEX(...));
            ??????????? }
            ???????? }
            ????? }

            ????? if (!fOk && (dwIOError == WAIT_TIMEOUT)) {?? // Thread timed-out
            ???????? if (!ThreadHasIoPending()) {
            ??????????? // There isn't much for the server to do, and this thread
            ??????????? // can die because it has no outstanding I/O requests
            ??????????? fStayInPool = FALSE;
            ???????? }
            ????? }

            ????? if (fOk || (po != NULL)) {
            ???????? // Thread woke to process something; process it
            ???????? ...

            ???????? if (GetCPUUsage() > 90) {?????? // CPU usage is above 90%
            ??????????? if (!ThreadHasIoPending()) { // No pending I/O requests
            ?????????????? if (g_nThreadsCrnt > g_nThreadsMin)) { // Pool above min

            ????????????????? fStayInPool = FALSE;?? // Remove thread from pool
            ?????????????? }
            ??????????? }
            ???????? }
            ????? }
            ?? }

            ?? // Thread is leaving pool
            ?? InterlockedDecrement(&g_nThreadsBusy);
            ?? InterlockedDecrement(&g_nThreadsCurrent);
            ?? return(0);
            }

            注意:在這章稍早的時候(in the section “Canceling Queued Device I/O Requests”)。我說過:當線程退出的時候,系統(tǒng)自動取消所有已發(fā)起但處于掛起中的I/O請求。這就是為什么偽代碼像ThreadHasIoPending這樣的函數(shù)是必須的。如果線程還有未完成的I/O請求,就不要允許線程結(jié)束。
            許多服務(wù)都提供了管理工具,以使管理員可以通過此來控制線程池的行為。例如,設(shè)置線程數(shù)的最小值和最大值,CPU使用極限,以及最大并發(fā)線程數(shù)。

            Simulating Completed I/O Requests
            I/O完成端口并不是只能用于device I/O,它也是用來解決線程間通訊問題的卓越的機制。
            BOOL PostQueuedCompletionStatus(
            ?? HANDLE????? hCompPort,
            ?? DWORD?????? dwNumBytes,
            ?? ULONG_PTR?? CompKey,
            ?? OVERLAPPED* pOverlapped);

            這個函數(shù)將一個“已完成的I/O”通知添加到一個完成端口的隊列上。第一個參數(shù)指定要針對的完成端口,剩下的三個參數(shù),dwNumBtyes,ComKey和pOverlapped指出了線程調(diào)用 GetQueuedCompletionStatus時將從對應(yīng)的返回參數(shù)中返回的值。當一個線程從“I/O完成隊列”中取出一條模擬記錄時,GetQueuedCompletionStatus函數(shù)將返回TRUE,表明已經(jīng)成功地執(zhí)行了一個I/O請求。
            函數(shù)PostQueuedCompletionStatus難以置信的有用,它提供了一種方式讓你可以跟線程池里的所有線程通訊。例如,當用戶終止服務(wù)應(yīng)用程序時,你當然希望所有的線程都能夠干干凈凈地退出。但如果有線程等待在完成端口上,并且此時又一直沒有I/O請求到達,那么這些線程就不能被喚醒。通過對線程池中的每個線程調(diào)用一次PostQueuedCompletionStatus,每個線程都能得以喚醒,你就可以設(shè)計檢查從GetQueuedCompletionStatus的返回參數(shù)中得到的值(比如設(shè)定完成鍵),可以判斷出是否是程序要終止了,由此可決定是否要做清理工作并退出。
            當使用這項技術(shù),你必須要特別小心。我的例子能工作是因為在池中的線程會死去并不會再次調(diào)用GetQueuedCompletionStatus。不管怎樣,如果你想要通知其池中的每個線程一些事情,并且它們會循環(huán)到再次調(diào)用GetQueuedCompletionStatus,你將會有一個問題。因為線程是按LIFO的順序醒來。所以你的應(yīng)用程序中你將會使用一些額外的線程同步來確保每個線程獲得了合適的機會來查看它的偽I/O記錄。不使用這個格外的線程同步,一個線程可能會查看相同的通知好幾次。


            ?from:油庫七號
            posted on 2008-10-29 18:36 我風(fēng) 閱讀(1620) 評論(0)  編輯 收藏 引用

            只有注冊用戶登錄后才能發(fā)表評論。
            網(wǎng)站導(dǎo)航: 博客園   IT新聞   BlogJava   博問   Chat2DB   管理


            <2025年6月>
            25262728293031
            1234567
            891011121314
            15161718192021
            22232425262728
            293012345

            常用鏈接

            留言簿(12)

            隨筆分類

            隨筆檔案

            文章檔案

            相冊

            收藏夾

            C++

            MyFavorite

            搜索

            •  

            積分與排名

            • 積分 - 327135
            • 排名 - 75

            最新評論

            閱讀排行榜

            評論排行榜

            中文字幕久久精品无码| 99精品国产在热久久无毒不卡| 久久久久久曰本AV免费免费| 国产精品免费久久久久影院 | 久久夜色精品国产噜噜噜亚洲AV| 久久精品无码专区免费| 99国内精品久久久久久久| 91精品国产91久久| 国产精品伦理久久久久久| 久久99热这里只有精品国产| 99久久免费只有精品国产| 久久精品三级视频| 理论片午午伦夜理片久久 | 久久精品国产精品亚洲下载 | 久久久国产99久久国产一| 久久91精品国产91| 一本色综合网久久| 久久精品成人免费网站| 久久WWW免费人成—看片| 亚洲国产视频久久| 日韩av无码久久精品免费| 久久综合中文字幕| 亚洲国产天堂久久久久久| 久久亚洲AV成人无码电影| 四虎国产精品免费久久久| 伊人久久成人成综合网222| 2021最新久久久视精品爱| 久久综合综合久久狠狠狠97色88| 久久成人国产精品一区二区| 7777精品久久久大香线蕉| 国产一级做a爰片久久毛片| 久久免费观看视频| 久久一日本道色综合久久| 久久久久国产成人精品亚洲午夜| 国产精品99久久久久久宅男小说| 99久久精品午夜一区二区| 亚洲国产一成久久精品国产成人综合| 久久www免费人成看片| 久久精品中文字幕第23页| 国产亚洲精品美女久久久| 久久综合给合综合久久|