• <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 - 35, comments - 115, trackbacks - 0, articles - 0
               :: 首頁 :: 新隨筆 :: 聯系 :: 聚合  :: 管理

            6課:多任務    下載源代碼


            聲明:轉載請保留:

            譯者:http://www.shnenglu.com/jinglexy

            原作者:xiaoming.mo at skelix dot org

            MSN & Email: jinglexy at yahoo dot com dot cn


            目標

             

            在本課中,我們將在skelix內核中同時運行多個任務(本文中可以暫時理解成進程)。每個任務都有自己的LDT表,并且是基于優先級的任務調度。


            現在我們開始skelix內核的多任務支持工作了。首先要澄清的是:在單處理器上面,進程只能并發,而不是并行。就是說同一時刻只能有一個任務在運行,cpu會劃分很多很小的時間片來運行各個任務,這樣看起來就像多個任務在同時運行一樣。i386從硬件上就支持多任務了,但是也可以通過編程來實現,本文就是一個例子,每個任務輪流運行一個小的時間片。我們知道,單個cpu只有1組寄存器,這些可以用于所有任務。當由一個任務切換到另一個任務時,必須先保存原來任務的運行環境(就是一堆寄存器和其他信息),通常稱這個運行環境叫上下文i386使用一個叫TSS的段來存儲這些信息,這個TSS段至少104字節(不包括IOPL位圖的話),對應它的是一個TSS描述符。TSS描述符只能在GDT表中,這意味著任務不能自己通過LDT來切換到其他任務。當時鐘中斷到來切換任務時,CPU會自動保存相關信息到TSS中。

            TSS
            定義了任務的上下文環境,結構如下:

             

                              (圖-1

             

                     31       16,15          0
                    +------------------------+                |
                    |   NO use  |  Back Link |                |
            高地址
                    +------------------------+                |
                    |         ESP0           |                |
                    +------------------------+                |
                    |   NO use  |    SS0     |                |
                    +------------------------+                |
                    |         ESP1           |                |
                    +------------------------+                |
                    |   NO use  |    SS1     |                |
            低地址
                    +------------------------+               \|/
                    |         ESP2           |
                    +------------------------+
                    |   NO use  |    SS2     |
                    +------------------------+
                    |         CR3            |
                    +------------------------+
                    |         EIP            |
                    +------------------------+
                    |        EFLAGS          |
                    +------------------------+
                    |         EAX            |
                    +------------------------+
                    |         ECX            |
                    +------------------------+
                    |         EDX            |
                    +------------------------+
                    |         EBX            |
                    +------------------------+
                    |         ESP            |
                    +------------------------+
                    |         EBP            |
                    +------------------------+
                    |         ESI            |
                    +------------------------+
                    |         EDI            |
                    +------------------------+
                    |   NO use  |    ES      |
                    +------------------------+
                    |   NO use  |    CS      |
                    +------------------------+
                    |   NO use  |    SS      |
                    +------------------------+
                    |   NO use  |    DS      |
                    +------------------------+
                    |   NO use  |    FS      |
                    +------------------------+
                    |   NO use  |    GS      |
                    +------------------------+

                    |   NO use  |    LDT     |
                    +------------------------+
                    |  I/O
            位圖 NO use | T |
                    +------------------------+

            i386
            處理器可以使用嵌套任務,就是說當任務1切換到任務2時,任務2 Back Link 域被設置成任務1的選擇子,且任務2EFLAGS中的NT(嵌套任務標識)置位,這樣任務2返回時,cpu就知道切換回任務1。我們知道,i3864中特權級,所以可以在TSS中設置這四種特權級下的堆棧。比如一個任務運行在ring3特權級下,它使用用戶態堆棧,使用系統調用可以讓這個任務切換到ring0的內核態,這是堆棧也必須切換到內核態堆棧。這些堆棧指針就是存放在TSS中的。現在我們來看看TSS描述符(它只能放在GDT中):

             

                               圖-2TSS描述符定義

             

             63_______________56__55__54__53__52__51_____________48_

            基地址(3124位) | G | 0 | 0 |AVL| 長度(1916位) |

            |_______________________________________________________|

             

             _47__46__45__44________41__40____39_________________32_

            | P |  DPL | 0 | 1 | 0 | B |  1  |  基地址(2316位)  |

            |_______________________________________________________|

             

             31____________________________________________________16

            |                    基地址(150位)                  |

            |_______________________________________________________|

             

             16_____________________________________________________

            |                    長度(150位)                    |

            |_______________________________________________________|

             

            *********************************************************

                               圖-3通用描述符定義

             

             63_______________56__55__54__53__52__51_____________48_

            基地址(3124位) | G |D/B| X | U | 長度(1916位) |

            |_______________________________________________________|

             

             _47__46__45__44____41______40____39_________________32_

            | P |  DPL |     類型    |  A   |  基地址(2316位)   |

            |_______________________________________________________|

             

             31____________________________________________________16

            |                    基地址(150位)                  |

            |_______________________________________________________|

             

             16_____________________________________________________

            |                    長度(150位)                    |

            |_______________________________________________________|

             

            我們現在來比較一下通用描述符和TSS描述符,在TSS描述符中,我們看到D位(數據還是代碼段)和X位(未使用)都被置為0,而且AVL位有效。類型type010B,第41位,及B位是TSS描述符的忙標識,因為一個進程只能有一個TSS,所以當進程執行時,B位將被設置,表示這個TSS正在被使用,除了進程調度外,不能再使用它了。A位被置為1,即便A位被置為可寫,TSS也不能被進程讀或寫。TSS的其他域和通用描述符一樣,只是要注意一點:就是TSS的長度界限至少要有104字節。另外需要注意的是,skelix暫時不支持fpummxsse,如果讀者想擴展的話需要注意每個進程都有自己的fpusse環境。

             

            在本課中,TSS描述符的DPL將被置為0,所以只有內核可以進程任務切換。和GDTRLDTR一樣,TSS描述符也有自己的用于切換任務的寄存器,就是TR。可以用LTR指令來加載這個寄存器。GDT中的TSS描述符不能被加載到任何其他寄存器,否則會發生一個異常。

            切換任務有多種方法,我們的做法是使用jmp指令跳轉到TSS描述符。(還有其他方法,如從中斷返回,使用任務門(起始也相當于jmpTSS上))。趙博的linux內核完全剖析上講到了切換任務的幾種方法,并詳細討論了,不清楚的同學可以購買該書。

            在切換任務時,CPU首先保存當前進程的上下文環境到當前TSS中,然后加載新任務的TSSTR寄存器中,然后從新任務的TSS中加載上下文到寄存器中,最后跳轉到新任務的第一條指令并開始執行。不知道這樣有沒有說清楚,這個是比較關鍵的地方,歡迎和我討論:jinglexy at yahoo dot com dot cnMSN)。

            OK
            ,讓程序來說明一起吧。先看看TSS數據結構的定義:
            06/include/task.h

             

            struct TSS_STRUCT {
                int    back_link;
                int    esp0, ss0;
                int    esp1, ss1;
                int    esp2, ss2;
                int    cr3;
                int    eip;
                int    eflags;
                int    eax,ecx,edx,ebx;
                int    esp, ebp;
                int    esi, edi;
                int    es, cs, ss, ds, fs, gs;
                int    ldt;
                int    trace_bitmap;
            };

            沒有pad的數據結構是104字節,如果你使用或模擬 IA64 平臺的話,就需要查找相關的資料了。

            對于一個正在運行的OS,上面的信息顯然是不夠的,所有我定義了另外一個包裝TSS的數據結構,

            即進程信息(process control block),以后簡稱為pcb

            #define TS_RUNNING    0                // 任務的三種狀態
            #define TS_RUNABLE    1
            #define TS_STOPPED    2

            struct TASK_STRUCT {                   // pcb
            定義
                struct TSS_STRUCT tss;
                unsigned long long tss_entry;      // tss
            描述符在gdt中的8字節入口項
                unsigned long long ldt[2];
                unsigned long long ldt_entry;
                int state;
                int priority;
                struct TASK_STRUCT *next;
            };
            #define DEFAULT_LDT_CODE    0x00cffa000000ffffULL
            #define DEFAULT_LDT_DATA    0x00cff2000000ffffULL
            #define INITIAL_PRIO        200

             

            priority 表示任務優先級,新建任務默認的優先級是下面的 INITIAL_PRIOskelix內核中所有的任務都用一個單向鏈表來表示,這樣做起來比較簡單。我們現在來看一個任務的例子,就是任務0:內核初始化完成后,就執行任務0


            06/task.c

            static unsigned long TASK0_STACK[256] = {0xf};

            上面數據是任務0在特權級0下使用的堆棧,0xf值如果不設置的話,這塊內存就會發生一些錯誤,我也不清楚是為什么,所以第一個元素隨便給了一個非0值。

            struct TASK_STRUCT TASK0 = {

                /* tss */
                {
                    0,
                    /* esp0                                    ss0 */
                    (unsigned)&TASK0_STACK+sizeof TASK0_STACK, DATA_SEL,

                    上面定義會使任務的esp0執行棧底,使用內核數據段選擇子就可以了
                    /* esp1 ss1 esp2 ss2 */
                    0, 0, 0, 0,
                    /* cr3 */
                    0,
                    /* eip eflags */
                    0, 0,
                    /* eax ecx edx ebx */
                    0, 0, 0, 0,
                    /* esp ebp */
                    0, 0,
                    /* esi edi */
                    0, 0,
                    /* es          cs             ds */
                    USER_DATA_SEL, USER_CODE_SEL, USER_DATA_SEL,
                    /* ss          fs             gs */
                    USER_DATA_SEL, USER_DATA_SEL, USER_DATA_SEL, 
                    /* ldt
            :見后面說明 */
                    0x20,
                    /* trace_bitmap */
                    0x00000000},
                /* tss_entry */
                0,
                /* ldt[2] */
                {DEFAULT_LDT_CODE, DEFAULT_LDT_DATA},
                /* ldt_entry */
                0,
                /* state */
                TS_RUNNING,
                /* priority */
                INITIAL_PRIO,
                /* next */
                0,
            };

            現在,我們定義了一個TSS,再在GDT中加入描述符索引:


            06/bootsect.s

            gdt:
                    .quad    0x0000000000000000 # null descriptor
                    .quad    0x00cf9a000000ffff # cs
                    .quad    0x00cf92000000ffff # ds
                    .quad    0x0000000000000000 # reserved for further use
                    .quad    0x0000000000000000 # reserved for further use

            第四項(0x3)用于存放該任務的TSS,所以定義了一個宏:  CURR_TASK_TSS = 3來索引GDT中的這個TSS。當任務釋放控制權后,會加載自己的TSS描述符索引到pcbtss_entry域。不管多少任務,都可以只使用兩個描述符,切換任務前更新GDT中的描述符即可。由于GDT有長度限制,只能存放8096個描述符,linux操作系統限制了任務的數量,我不知道為什么要這樣做,因為突破進程數限制看起來如此的簡單。


            06/task.c

            unsigned long long set_tss(unsigned long long tss) {             // 參數為tss在內存中的地址
                unsigned long long __tss_entry = 0x0080890000000067ULL;
                __tss_entry |= ((tss)<<16) & 0xffffff0000ULL;                //
            基地址低24
                __tss_entry |= ((tss)<<32) & 0xff00  0000  0000   0000ULL;   //
            基地址高8
                return gdt[CURR_TASK_TSS] = __tss_entry;
            }

            這個函數產生TSS描述符,并存放它到GDT中,可以看到,描述符的DPL設置為0,所以只有內核可以用這個描述符。

            屬性0080890000000067分析:0x67是長度103(從0開始計算,即長104),89p位為1dpl位為0b位為180是粒度G位為1


            unsigned long long get_tss(void) {
                return gdt[CURR_TASK_TSS];
            }

            LDT

            我們看了這么多關于GDTLDT的代碼后,LDT非常容易理解了。它和GDT差不多,區別是具有局部特性,每個任務都可以有自己的LDT,我們在skelix內核中為每個任務在LDT中設置兩個描述符項,第一項是代碼段,第二項是數據段和堆棧段,描述符選擇子格式如下:

                        圖-4

             15______________________________3___2____1___0__

            |              Index              | TI |   RPL  |

            |_______________________________________________|

             

            我們設置所有任務的特權級為3,即RPL 11b,且TI 1(表示使用LDT而不是GDT),和GDT不同的是,LDT的第一項可以使用而不是保留,所以代碼段的選擇子為0x7,而數據段和堆棧段選擇子為0xf

            TSS
            數據結構中,有一個LDT域,保存的是GDT表中的描述符。聽起來可能昏菜,我們現在來搞明白她。首先,每個進程都有自己的LDT表,而且這個表可以在內存的任何地方(暫時不考慮虛擬內存),所以需要一個描述符來索引這個內存地址(即LDT表),這個描述符就放在GDT中,并且在TSS中存放一份該描述符的選擇子。

            畫個圖來說明好了:

                        圖-5

             

             ________________                            ________________

            |      TASK      |                          |      GDT       |

            |________________|                          |                |

            TSS  __________|/    選擇子存于此         |________________|

            |     | LDT field|--------------------------|     描述符     |

            |      ----------|\                        /|________________|

            |________________|                        / |                |

            |                |                       /  |                |

            |                |     描述符索引該 LDT /   |                |

            |                |         -------------    |                |

            |                |        /                 |________________|

            |                |       /

            |                |      /

            |                |     /

            |                |    /

            |                |   /

            |                |  /

            |________________| /

            |      LDT       |/

            |________________|

            |________________|

            GDT
            中的第三項用于任務TSS,第四項用于任務LDT。通過選擇子0x180x20分包可以索引到它們。和設置TSS一樣,對應也寫有兩個LDT操作的函數。

            06/task.c

             

            unsigned long long
            set_ldt(unsigned long long ldt) {
                unsigned long long ldt_entry = 0x008082000000000fULL;        // DPL
            3而不是0

                ldt_entry |= ((ldt)<<16) & 0xffffff0000ULL;
                ldt_entry |= ((ldt)<<32) & 0xff00000000000000ULL;
                return gdt[CURR_TASK_LDT] = ldt_entry;
            }

            unsigned long long
            get_ldt(void) {
                return gdt[CURR_TASK_LDT];
            }

            現在,我們將設置所有任務使用相同的LDT,這些任務共享相同的內存空間。如果你要設計字節的OS,這不會是個好主意。但是后面會為這些任務通過虛擬內存機制來設置不同的內存空間,后續課程會講到。

            06/include/task.h

             

            #define DEFAULT_LDT_CODE    0x00cffa000000ffffULL
            #define DEFAULT_LDT_DATA    0x00cff2000000ffffULL

            上面就是任務的LDT描述符定義,注意DPLdescriptor priority level)值為3

             

            上面已經提到,所有任務使用一個單向鏈表連接起來,其中有兩個重要指針:一個是任務0TASK0)的next指針,它是所有任務鏈表的頭指針;另一個是current指針,指向當前正在運行的任務。

            創建任務并調度

            首先,我們定義有:
            06/task.c

            struct TASK_STRUCT *current = &TASK0;

             

            現在我們來看看新任務是如何創建的:
            06/init.c

            static void
            new_task(struct TASK_STRUCT *task, unsigned int eip,
                            unsigned int stack0, unsigned int stack3) {

            // 這個函數有4個參數:第一個參數是任務數據結構的內存地址

            // 第二個參數是任務入口地址

            // 第三個和第四個參數是0環和3環特權級下堆棧地址

            // 由于堆棧地址的描述符選擇子是固定的,所以就不用傳進來了

                memcpy(task, &TASK0, sizeof(struct TASK_STRUCT));        // TASK0
            作為任務模板
                task->tss.esp0 = stack0;
                task->tss.eip = eip;
                task->tss.eflags = 0x3202;        //
            合適的狀態標識
                task->tss.esp = stack3;

                task->priority = INITIAL_PRIO;    //
            新任務默認優先級

                task->state = TS_STOPPED;
                task->next = current->next;
                current->next = task;
                task->state = TS_RUNABLE;
            }
            extern void task1_run(void);          // 
            任務入口函數
            extern void task2_run(void);

            static long task1_stack0[1024] = {0xf, };
            static long task1_stack3[1024] = {0xf, };
            static long task2_stack0[1024] = {0xf, };
            static long task2_stack3[1024] = {0xf, };

            // 因為沒有內核中沒有實現內存管理,所以固定設置一些任務和她們使用的堆棧。

            // 任務0運行在內核態,任務1和任務2運行在ring3

            void
            init(void) {
                char wheel[] = {'\\', '|', '/', '-'};
                int i = 0;
                struct TASK_STRUCT task1;                      //
            移到全局定義較合適
                struct TASK_STRUCT task2;

                idt_install();
                pic_install();
                kb_install();
                timer_install(100);

                set_tss((unsigned long long)&TASK0.tss);       // 讓任務0先跑起來
                set_ldt((unsigned long long)&TASK0.ldt);

                __asm__ ("ltrw    %%ax\n\t"::"a"(TSS_SEL));    // 加載任務0使用的TRLDTR寄存器
                __asm__ ("lldt    %%ax\n\t"::"a"(LDT_SEL));    //
            使用ltrwlldt指令

             

                sti();                                         // 從現在開始捕獲中斷或異常
                new_task(&task1,                               //
            注意:創建任務時,中斷是使能的
                        (unsigned int)task1_run,
                        (unsigned int)task1_stack0+sizeof task1_stack0,
                        (unsigned int)task1_stack3+sizeof task1_stack3);
                new_task(&task2,
                        (unsigned int)task2_run,
                        (unsigned int)task2_stack0+sizeof task2_stack0,
                        (unsigned int)task1_stack3+sizeof task2_stack3);

             

                __asm__ ("movl %%esp,%%eax\n\t" \
                         "pushl %%ecx\n\t" \         //
            任務0內核棧ss
                         "pushl %%eax\n\t" \         //
            任務0內核棧esp0
                         "pushfl\n\t" \              // eflags
                         "pushl %%ebx\n\t" \         //
            任務0代碼段選擇子cs
                         "pushl $1f\n\t" \           // 
            任務0函數地址eip:就是下面第2行匯編
                         "iret\n" \
                         "1:\tmovw %%cx,%%ds\n\t" \
                         "movw %%cx,%%es\n\t" \
                         "movw %%cx,%%fs\n\t" \
                         "movw %%cx,%%gs" \
                         ::"b"(USER_CODE_SEL),"c"(USER_DATA_SEL));

                // 注意:現在開始內核運行在任務0上了,通過iret指令長跳轉到任務0

                // (先加載正確的EIPCSSSESP,圖如下:)
                +--------------------+
                | LDT stack selector |
                +--------------------+
                |        ESP         |
                +--------------------+
                |       EFLAGS       |
                +--------------------+
                | LDT code selector  |
                +--------------------+
                |        EIP         |
                +--------------------+

             

                for (;;) {
                    __asm__ ("movb    %%al,    0xb8000+160*24"::"a"(wheel[i]));
                    if (i == sizeof wheel)
                        i = 0;
                    else
                        ++i;
                }
            }

            現在,任務已經有了,萬事具備只欠東風了,下面加入時鐘中斷代碼進行任務調度。
            06/timer.c

            void do_timer(void) {        // 時鐘中斷處理函數
                struct TASK_STRUCT *v = &TASK0;
                int x, y;
                ++timer_ticks;
                get_cursor(&x, &y);
                set_cursor(71, 24);
                kprintf(KPL_DUMP, "%x", timer_ticks);
                set_cursor(x, y);
                outb(0x20, 0x20);
                cli();
                for (; v; v=v->next) {   //
            遍歷鏈表,調整任務優先級
                    if (v->state == TS_RUNNING) {
                        if ((v->priority+=30) <= 0)
                            v->priority = 0xffffffff;
                    } else
                        v->priority -= 10;    // 
            值越低,優先級越高(等待的任務優先級會變高)

                    // *nix內核通常這樣做:較小的值優先級較高
                }

                if (! (timer_ticks%1))
                    scheduler();

                sti();
            }

             

            調度函數實現:
            06/task.c

            void scheduler(void) {
                struct TASK_STRUCT *v = &TASK0, *tmp = 0;
                int cp = current->priority;

                for (; v; v = v->next) {
                    if ((v->state==TS_RUNABLE) && (cp>v->priority)) {
                        tmp = v;
                        cp = v->priority;
                    }
                }    //
            遍歷鏈表,找尋優先級最高的任務(即值最小的任務)

                if (tmp && (tmp!=current)) {           // tmp
            是遍歷的結果
                    current->tss_entry = get_tss();    //
            TSS LDT描述符
                    current->ldt_entry = get_ldt();
                    tmp->tss_entry = set_tss((unsigned long long)((unsigned int)&tmp->tss));
                    tmp->ldt_entry = set_ldt((unsigned long long)((unsigned int)&tmp->ldt));


                    current->state = TS_RUNABLE;
                    tmp->state = TS_RUNNING;
                    current = tmp;

                    __asm__ __volatile__("ljmp    $" TSS_SEL_STR ",    $0\n\t");

                    // skelix通過長跳轉到TSS描述符上(偏移應為0)切換任務
                }
            }


            下面是任務 A & B

            06/isr.s

            task1_run:
                    call    do_task1
                    jmp     task1_run
            task2_run:
                    call    do_task2
                    jmp     task2_run

            這里使用匯編而不是c語言來寫這兩個任務的入口函數的原因,

            是不想在跳轉前后改變內核棧。

             

            我們現在看看能不能在任務1和任務2中使用kprintf
            06/init.c

            void
            do_task1(void) {
                unsigned int cs;
                __asm__ ("movl    %%cs,    %%eax":"=a"(cs));
                kprintf(KPL_DUMP, "%x", cs);
                for (;;)
                    ;
            }

            void
            do_task2(void) {
                unsigned int cs;
                __asm__ ("movl    %%cs,    %%eax":"=a"(cs));
                kprintf(KPL_PANIC, "%x", cs);
                for (;;)
                    ;
            }

             

            修改Makefile 中的 KERNEL_OBJS
            06/MakefileKERNEL_OBJS= load.o init.o isr.o timer.o libcc.o scr.o kb.o task.o kprintf.o exceptions.o

            編譯,運行一把。截圖就不貼出來了(結果是看不到任務1和任務2的打印:ring3任務當然不能使用ring0函數)

            ok,現在改一下這兩個任務函數:
            06/init.cvoid
            do_task1(void) {
                print_c('A', BLUE, WHITE);
            }

            void
            do_task2(void) {
                print_c('B', GRAY, BROWN);
            }

             

            編譯,運行,正是我們想要的結果。

             

             

            Feedback

            # re: 自己動手寫內核(第6課:多任務)(原創)  回復  更多評論   

            2007-05-18 19:06 by raywill
            加油哦~

            快快去實現內存管理,文件系統~

            那才是最有挑戰的地方!Fighting!

            ------------
            raywill.blog.sohu.com

            # re: 自己動手寫內核(第6課:多任務)(原創)  回復  更多評論   

            2007-05-18 19:19 by raywill
            不過,發現你這都是轉載吧.....

            skelix....

            請注明 :(


            # re: 自己動手寫內核(第6課:多任務)(原創)[未登錄]  回復  更多評論   

            2007-05-19 09:32 by 天衣有縫
            沒錯,翻譯文章,原作者只在第一篇聲明了,所有文章整理完的時候加上去把,免得對不起作者了,相對來說我做的事情就少很多。

            歡迎批評指正
            久久人人爽人人人人片av| 久久精品中文字幕久久| 青草久久久国产线免观| 一本大道久久东京热无码AV| 久久男人Av资源网站无码软件| 久久青青草原国产精品免费 | 久久99精品免费一区二区| 久久夜色撩人精品国产小说| 久久精品a亚洲国产v高清不卡| 久久99精品综合国产首页| 久久精品国产99久久久古代| 精品久久久久久中文字幕| 污污内射久久一区二区欧美日韩| 囯产极品美女高潮无套久久久| 久久久久噜噜噜亚洲熟女综合| 一本久久a久久精品综合夜夜| 久久久久久国产精品无码超碰| 国产三级观看久久| 天天久久狠狠色综合| 狠狠色婷婷久久综合频道日韩 | 久久精品中文字幕有码| 看久久久久久a级毛片| 囯产精品久久久久久久久蜜桃| 99久久免费国产精品| 7777久久亚洲中文字幕| 国产99久久精品一区二区| 奇米影视7777久久精品人人爽 | 婷婷国产天堂久久综合五月| 国内精品久久久久影院网站| 久久久久人妻精品一区二区三区| 欧美久久久久久| 色偷偷88欧美精品久久久| 国产综合免费精品久久久| 国产精品激情综合久久 | 久久久精品日本一区二区三区| 69国产成人综合久久精品| 久久精品国产第一区二区三区| 久久人妻少妇嫩草AV无码专区| 久久综合给合久久狠狠狠97色69| 精品无码久久久久国产动漫3d| 久久久久久国产a免费观看黄色大片 |