一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

淺談設(shè)備驅(qū)動Linux操作系統(tǒng)學(xué)習(xí)之字符設(shè)備

 山峰云繞 2021-12-14

https://m.toutiao.com/is/R3wEfuv/?=淺談Linux操作系統(tǒng)學(xué)習(xí)之字符設(shè)備 


一. 前言

上文中我們分析了虛擬文件系統(tǒng)的結(jié)構(gòu)以及常見的文件操作從用戶態(tài)到虛擬文件系統(tǒng)再到底層實際文件系統(tǒng)的過程。而實際上我們并沒有說明實際的文件系統(tǒng)如ext4是如何和磁盤進行交互的,這就是本文和下篇文章的重點:I/O之塊設(shè)備和字符設(shè)備。輸入輸出設(shè)備我們大致可以分為兩類:塊設(shè)備(Block Device)和字符設(shè)備(Character Device)。

  • 塊設(shè)備將信息存儲在固定大小的塊中,每個塊都有自己的地址。如硬盤就是常見的塊設(shè)備。
  • 字符設(shè)備發(fā)送或接收的是字節(jié)流,而不用考慮任何塊結(jié)構(gòu),沒有辦法尋址。如鼠標(biāo)就是常見的字符設(shè)備。

本文首先介紹虛擬文件系統(tǒng)下層直至硬件輸入輸出設(shè)備的結(jié)構(gòu)關(guān)系,然后重點分析字符設(shè)備相關(guān)的整體邏輯情況。

二. I/O架構(gòu)

由于各種輸入輸出設(shè)備具有不同的硬件結(jié)構(gòu)、驅(qū)動程序,因此我們采取了設(shè)備控制器這一中間層對上提供統(tǒng)一接口。設(shè)備控制器通過緩存來處理CPU和硬件I/O之間的交互關(guān)系,通過中斷進行通知,因此我們需要有中斷處理器對各種中斷進行統(tǒng)一。由于每種設(shè)備的控制器的寄存器、緩沖區(qū)等使用模式,指令都不同,所以對于操作系統(tǒng)還需要一層對接各個設(shè)備控制器的設(shè)備驅(qū)動程序。

這里需要注意的是,設(shè)備控制器不屬于操作系統(tǒng)的一部分,但是設(shè)備驅(qū)動程序?qū)儆诓僮飨到y(tǒng)的一部分。操作系統(tǒng)的內(nèi)核代碼可以像調(diào)用本地代碼一樣調(diào)用驅(qū)動程序的代碼,而驅(qū)動程序的代碼需要發(fā)出特殊的面向設(shè)備控制器的指令,才能操作設(shè)備控制器。設(shè)備驅(qū)動程序中是一些面向特殊設(shè)備控制器的代碼,不同的設(shè)備不同。但是對于操作系統(tǒng)其它部分的代碼而言,設(shè)備驅(qū)動程序有統(tǒng)一的接口。

設(shè)備驅(qū)動本身作為一個內(nèi)核模塊,通常以ko文件的形式存在,它有著其獨特的代碼結(jié)構(gòu):

  • 頭文件部分,設(shè)備驅(qū)動程序至少需要以下頭文件
#include <linux/module.h>#include <linux/init.h>
  • 調(diào)用MODULE_LICENSE聲明lisence
  • 初始化函數(shù)module_init和退出函數(shù)module_exit的定義,用于加載和卸載ko文件
  • 文件系統(tǒng)的接口file_operation結(jié)構(gòu)體定義
  • 定義需要的函數(shù)

在下文的分析中,我們就將按照此順序來剖析字符設(shè)備的源碼,以弄懂字符設(shè)備的一般運行邏輯。關(guān)于設(shè)備驅(qū)動代碼編寫的詳細(xì)知識可以參考《Linux設(shè)備驅(qū)動》一書,本文重點不在于如何編寫代碼,而是在于操作系統(tǒng)中的字符設(shè)備和塊設(shè)備如何工作。

更多Linux內(nèi)核視頻教程文檔資料免費領(lǐng)取后臺私信【內(nèi)核】自行獲取。

內(nèi)核學(xué)習(xí)網(wǎng)站:

Linux內(nèi)核源碼/內(nèi)存調(diào)優(yōu)/文件系統(tǒng)/進程管理/設(shè)備驅(qū)動/網(wǎng)絡(luò)協(xié)議棧-學(xué)習(xí)視頻教程-騰訊課堂

三. 字符設(shè)備基本構(gòu)成

一個字符設(shè)備由3個部分組成:

  • 封裝對于外部設(shè)備的操作的設(shè)備驅(qū)動程序,即 ko 文件模塊,里面有模塊初始化函數(shù)、中斷處理函數(shù)、設(shè)備操作函數(shù)等。加載設(shè)備驅(qū)動程序模塊的時候,模塊初始化函數(shù)會被調(diào)用。在內(nèi)核維護所有字符設(shè)備驅(qū)動的數(shù)據(jù)結(jié)構(gòu) cdev_map 里面注冊設(shè)備號后,我們就可以很容易根據(jù)設(shè)備號找到相應(yīng)的設(shè)備驅(qū)動程序。
  • 特殊的設(shè)備驅(qū)動文件系統(tǒng) devtmpfs,在/dev 目錄下生成一個文件表示這個設(shè)備。打開一個字符設(shè)備文件和打開一個普通的文件有類似的數(shù)據(jù)結(jié)構(gòu),有文件描述符、有 struct file、指向字符設(shè)備文件的 dentry 和 inode。其對應(yīng)的 inode 是一個特殊的 inode,里面有設(shè)備號。通過它我們可以在 cdev_map 中找到設(shè)備驅(qū)動程序,里面還有針對字符設(shè)備文件的默認(rèn)操作 def_chr_fops。
  • 字符設(shè)備文件的相關(guān)操作 file_operations 。一開始會指向 def_chr_fops,在調(diào)用 def_chr_fops 里面的 chrdev_open 函數(shù)的時候修改為指向設(shè)備操作函數(shù),從而讀寫一個字符設(shè)備文件就會直接變成讀寫外部設(shè)備了。 ?? 這里主要涉及到了兩個結(jié)構(gòu)體:字符設(shè)備信息存儲的struct cdev以及管理字符設(shè)備的cdev_map。
struct cdev {    struct kobject kobj;                  //內(nèi)嵌的內(nèi)核對象.    struct module *owner;                 //該字符設(shè)備所在的內(nèi)核模塊的對象指針.    const struct file_operations *ops;    //該結(jié)構(gòu)描述了字符設(shè)備所能實現(xiàn)的方法,是極為關(guān)鍵的一個結(jié)構(gòu)體.    struct list_head list;                //用來將已經(jīng)向內(nèi)核注冊的所有字符設(shè)備形成鏈表.    dev_t dev;                            //字符設(shè)備的設(shè)備號,由主設(shè)備號和次設(shè)備號構(gòu)成.    unsigned int count;                   //隸屬于同一主設(shè)備號的次設(shè)備號的個數(shù).} __randomize_layout;

cdev結(jié)構(gòu)體還有另一個相關(guān)聯(lián)的結(jié)構(gòu)體char_device_struct。這里首先會定義主設(shè)備號和次設(shè)備號:主設(shè)備號用來標(biāo)識與設(shè)備文件相連的驅(qū)動程序,用來反映設(shè)備類型。次設(shè)備號被驅(qū)動程序用來辨別操作的是哪個設(shè)備,用來區(qū)分同類型的設(shè)備。這里minorct指的是分配的區(qū)域,用于主設(shè)備號和次設(shè)備號的分配工作。

static struct char_device_struct { struct char_device_struct *next; unsigned int major; unsigned int baseminor; int minorct; char name[64]; struct cdev *cdev; /* will die */} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

cdev_map用于維護所有字符設(shè)備驅(qū)動,實際是結(jié)構(gòu)體kobj_map,主要包括了一個互斥鎖lock,一個probes[255]數(shù)組,數(shù)組元素為struct probe的指針,該結(jié)構(gòu)體包括鏈表項、設(shè)備號、設(shè)備號范圍等。所以我們將字符設(shè)備驅(qū)動最后保存為一個probe,并用cdev_map/kobj_map進行統(tǒng)一管理。

static struct kobj_map *cdev_map;struct kobj_map {    struct probe {        struct probe *next;        dev_t dev;        unsigned long range;        struct module *owner;        kobj_probe_t *get;        int (*lock)(dev_t, void *);        void *data;    } *probes[255];    struct mutex *lock;};

四. 打開字符設(shè)備

字符設(shè)備有很多種,這里以打印機設(shè)備為輸出設(shè)備的例子,源碼位于drivers/char/lp.c。以鼠標(biāo)為輸入設(shè)備的例子,源碼位于
drivers/input/mouse/logibm.c。下面將根據(jù)上述的字符設(shè)備的三個組成部分分別剖析如何創(chuàng)建并打開字符設(shè)備。

4.1 加載

字符設(shè)備的使用從加載開始,通常我們會使用insmod命令或者modprobe命令加載ko文件,ko文件的加載則從module_init調(diào)用該設(shè)備自定義的初始函數(shù)開始。對于打印機來說,其初始化函數(shù)定義為lp_init_module(),實際調(diào)用lp_init()。lp_init()會初始化打印機結(jié)構(gòu)體,并調(diào)用register_chardev()注冊該字符設(shè)備。

module_init(lp_init_module);static int __init lp_init_module(void){...... return lp_init();}static int __init lp_init(void){...... if (register_chrdev(LP_MAJOR, 'lp', &lp_fops)) { printk(KERN_ERR 'lp: unable to get major %d\n', LP_MAJOR); return -EIO; }......}

register_chrdev()實際調(diào)用__register_chrdev(),該函數(shù)會進行字符設(shè)備的注冊操作。其主要邏輯如下

  • 調(diào)用__register_chrdev_region()注冊字符設(shè)備的主設(shè)備號和名稱
  • 調(diào)用cdev_alloc()分配結(jié)構(gòu)體struct cdev
  • 將 cdev 的 ops 成員變量指向這個模塊聲明的 file_operations
  • 調(diào)用cdev_add()將這個字符設(shè)備添加到結(jié)構(gòu)體 struct kobj_map *cdev_map ,該結(jié)構(gòu)體用于統(tǒng)一管理所有字符設(shè)備。
static inline int register_chrdev(unsigned int major, const char *name,                  const struct file_operations *fops){    return __register_chrdev(major, 0, 256, name, fops);}int __register_chrdev(unsigned int major, unsigned int baseminor,              unsigned int count, const char *name,              const struct file_operations *fops){    struct char_device_struct *cd;    struct cdev *cdev;    int err = -ENOMEM;    cd = __register_chrdev_region(major, baseminor, count, name);    if (IS_ERR(cd))        return PTR_ERR(cd);    cdev = cdev_alloc();    if (!cdev)        goto out2;    cdev->owner = fops->owner;    cdev->ops = fops;    kobject_set_name(&cdev->kobj, '%s', name);    err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);......}// 拼接ma和mi#define MINORBITS	20#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))

對于鼠標(biāo)來說,加載也是類似的:注冊為logibm_init()函數(shù)。但是這里沒有調(diào)用register_chrdev()而是使用input_register_device(),原因在于輸入設(shè)備會統(tǒng)一由input_init()初始化,之后加入的輸入設(shè)備通過input_register_device()注冊到input的管理結(jié)構(gòu)體中進行統(tǒng)一管理。

module_init(logibm_init);static int __init logibm_init(void){...... err = input_register_device(logibm_dev);......}

4.2 創(chuàng)建文件設(shè)備

加載完ko文件后,Linux內(nèi)核會通過mknod在/dev目錄下創(chuàng)建一個設(shè)備文件,只有有了這個設(shè)備文件,我們才能通過文件系統(tǒng)的接口對這個設(shè)備文件進行操作。mknod本身是一個系統(tǒng)調(diào)用,主要邏輯為調(diào)用user_path_create()為該設(shè)備文件創(chuàng)建dentry,然后對于S_IFCHAR或者S_IFBLK會調(diào)用vfs_mknod()去調(diào)用對應(yīng)文件系統(tǒng)的操作。

SYSCALL_DEFINE3(mknod, const char __user *, filename, umode_t, mode, unsigned, dev){    return sys_mknodat(AT_FDCWD, filename, mode, dev);}SYSCALL_DEFINE4(mknodat, int, dfd, const char __user *, filename, umode_t, mode,    unsigned, dev){    struct dentry *dentry;    struct path path;......    dentry = user_path_create(dfd, filename, &path, lookup_flags);......    switch (mode & S_IFMT) {......        case S_IFCHR: case S_IFBLK:            error = vfs_mknod(path.dentry->d_inode,dentry,mode,            new_decode_dev(dev));        break;......  }}int vfs_mknod(struct inode *dir, struct dentry *dentry, umode_t mode, dev_t dev){......    error = dir->i_op->mknod(dir, dentry, mode, dev);......}

對于/dev目錄下的設(shè)備驅(qū)動來說,所屬的文件系統(tǒng)為devtmpfs文件系統(tǒng),即設(shè)備驅(qū)動臨時文件系統(tǒng)。devtmpfs對應(yīng)的文件系統(tǒng)定義如下

static struct file_system_type dev_fs_type = { .name = 'devtmpfs', .mount = dev_mount, .kill_sb = kill_litter_super,};static struct dentry *dev_mount(struct file_system_type *fs_type, int flags, const char *dev_name, void *data){#ifdef CONFIG_TMPFS return mount_single(fs_type, flags, data, shmem_fill_super);#else return mount_single(fs_type, flags, data, ramfs_fill_super);#endif}

從這里可以看出,devtmpfs 在掛載的時候有兩種模式:一種是 ramfs,一種是 shmem ,都是基于內(nèi)存的文件系統(tǒng)。這兩個 mknod 雖然實現(xiàn)不同,但是都會調(diào)用到同一個函數(shù) init_special_inode()。顯然這個文件是個特殊文件,inode 也是特殊的。這里這個 inode 可以關(guān)聯(lián)字符設(shè)備、塊設(shè)備、FIFO 文件、Socket 等。我們這里只看字符設(shè)備。這里的 inode 的 file_operations 指向一個 def_chr_fops,這里面只有一個 open,就等著你打開它。另外,inode 的 i_rdev 指向這個設(shè)備的 dev_t。通過這個 dev_t,可以找到我們剛剛加載的字符設(shè)備 cdev。

static const struct inode_operations ramfs_dir_inode_operations = {......  .mknod    = ramfs_mknod,};static const struct inode_operations shmem_dir_inode_operations = {#ifdef CONFIG_TMPFS......  .mknod    = shmem_mknod,};void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev){    inode->i_mode = mode;    if (S_ISCHR(mode)) {        inode->i_fop = &def_chr_fops;        inode->i_rdev = rdev;    } else if (S_ISBLK(mode)) {        inode->i_fop = &def_blk_fops;        inode->i_rdev = rdev;    } else if (S_ISFIFO(mode))        inode->i_fop = &pipefifo_fops;    else if (S_ISSOCK(mode))        ;  /* leave it no_open_fops */    else        printk(KERN_DEBUG 'init_special_inode: bogus i_mode (%o) for'                  ' inode %s:%lu\n', mode, inode->i_sb->s_id,                  inode->i_ino);    }const struct file_operations def_chr_fops = {    .open = chrdev_open,};

由此我們完成了/dev下文件的創(chuàng)建,并利用rdev和生成的字符設(shè)備進行了關(guān)聯(lián)。

4.3 打開字符設(shè)備

如打開普通文件一樣,打開字符設(shè)備也會首先分配對應(yīng)的文件描述符fd,然后生成struct file結(jié)構(gòu)體與其綁定,并將file關(guān)聯(lián)到對應(yīng)的dentry從而可以接觸inode。在進程里面調(diào)用 open() 函數(shù),最終會調(diào)用到這個特殊的 inode 的 open() 函數(shù),也就是 chrdev_open()。

chrdev_open()主要邏輯為;

  • 調(diào)用kobj_lookup(),通過設(shè)備號i_cdev關(guān)聯(lián)對應(yīng)的設(shè)備驅(qū)動程序
  • 調(diào)用fops_get()將設(shè)備驅(qū)動程序自己定義的文件操作p->ops賦值給fops
  • 調(diào)用設(shè)備驅(qū)動程序的 file_operations 的 open() 函數(shù)真正打開設(shè)備。對于打印機,調(diào)用的是 lp_open()。對于鼠標(biāo)調(diào)用的是 input_proc_devices_open(),最終會調(diào)用到 logibm_open()。
/* * Called every time a character special file is opened */static int chrdev_open(struct inode *inode, struct file *filp){ const struct file_operations *fops; struct cdev *p; struct cdev *new = NULL;...... p = inode->i_cdev;...... kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);...... fops = fops_get(p->ops);...... replace_fops(filp, fops); if (filp->f_op->open) { ret = filp->f_op->open(inode, filp);......}

上述過程借用極客時間中的圖來作為總結(jié)。

五. 寫入字符設(shè)備

寫入字符設(shè)備和寫入普通文件一樣,調(diào)用write()函數(shù)執(zhí)行。該函數(shù)在內(nèi)核里查詢系統(tǒng)調(diào)用表最終調(diào)用sys_write(),并根據(jù)fd描述符獲取對應(yīng)的file結(jié)構(gòu)體,接著調(diào)用vfs_write()去調(diào)用對應(yīng)的文件系統(tǒng)自定義的寫入函數(shù)file->f_op->write()。對于打印機來說,最終調(diào)用的是自定義的lp_write()函數(shù)。

這里寫入的重點在于調(diào)用 copy_from_user() 將數(shù)據(jù)從用戶態(tài)拷貝到內(nèi)核態(tài)的緩存中,然后調(diào)用 parport_write() 寫入外部設(shè)備。這里還有一個 schedule() 函數(shù),也即寫入的過程中,給其他線程搶占 CPU 的機會。如果寫入字節(jié)數(shù)多,不能一次寫完,就會在循環(huán)里一直調(diào)用 copy_from_user() 和 parport_write(),直到寫完為止。

static ssize_t lp_write(struct file *file, const char __user *buf,            size_t count, loff_t *ppos){    unsigned int minor = iminor(file_inode(file));    struct parport *port = lp_table[minor].dev->port;    char *kbuf = lp_table[minor].lp_buffer;    ssize_t retv = 0;    ssize_t written;    size_t copy_size = count;......    /* Need to copy the data from user-space. */    if (copy_size > LP_BUFFER_SIZE)        copy_size = LP_BUFFER_SIZE;......    if (copy_from_user(kbuf, buf, copy_size)) {        retv = -EFAULT;        goto out_unlock;    }......    do {        /* Write the data. */        written = parport_write(port, kbuf, copy_size);        if (written > 0) {            copy_size -= written;            count -= written;            buf  += written;            retv += written;        }......        if (need_resched())            schedule();         if (count) {            copy_size = count;            if (copy_size > LP_BUFFER_SIZE)                copy_size = LP_BUFFER_SIZE;            if (copy_from_user(kbuf, buf, copy_size)) {                if (retv == 0)                    retv = -EFAULT;                break;            }        }    } while (count > 0);......}

六. 字符設(shè)備的控制

在Linux中,我們常用ioctl()來對I/O設(shè)備進行一些讀寫之外的特殊操作。其參數(shù)主要由文件描述符fd,命令cmd以及命令參數(shù)arg構(gòu)成。其中cmd由幾個部分拼接成整型,主要結(jié)構(gòu)為

  • 最低8位為 NR,表示命令號;
  • 次低8位為 TYPE,表示類型;
  • 14位表示參數(shù)的大?。?/span>
  • 最高2位是 DIR,表示寫入、讀出,還是讀寫。

ioctl()也是一個系統(tǒng)調(diào)用,其中fd 是這個設(shè)備的文件描述符,cmd 是傳給這個設(shè)備的命令,arg 是命令的參數(shù)。主要調(diào)用do_vfs_ioctl()完成實際功能。

SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg){ int error; struct fd f = fdget(fd);...... error = do_vfs_ioctl(f.file, fd, cmd, arg); fdput(f); return error;}

??do_vfs_ioctl()對于已經(jīng)定義好的 cmd進行相應(yīng)的處理。如果不是默認(rèn)定義好的 cmd,則執(zhí)行默認(rèn)操作:對于普通文件,調(diào)用 file_ioctl,對于其他文件調(diào)用 vfs_ioctl。

/* * When you add any new common ioctls to the switches above and below * please update compat_sys_ioctl() too. * * do_vfs_ioctl() is not for drivers and not intended to be EXPORT_SYMBOL()'d. * It's just a simple helper for sys_ioctl and compat_sys_ioctl. */int do_vfs_ioctl(struct file *filp, unsigned int fd, unsigned int cmd,         unsigned long arg){    int error = 0;    int __user *argp = (int __user *)arg;    struct inode *inode = file_inode(filp);    switch (cmd) {    case FIOCLEX:        set_close_on_exec(fd, 1);        break;    case FIONCLEX:        set_close_on_exec(fd, 0);        break;    case FIONBIO:        error = ioctl_fionbio(filp, argp);        break;    case FIOASYNC:        error = ioctl_fioasync(fd, filp, argp);        break;......    default:        if (S_ISREG(inode->i_mode))            error = file_ioctl(filp, cmd, arg);        else            error = vfs_ioctl(filp, cmd, arg);        break;    }    return error;}

??對于字符設(shè)備驅(qū)動程序,最終會調(diào)用vfs_ioctl()。這里面調(diào)用的是 struct file 里 file_operations 的 unlocked_ioctl() 函數(shù)。我們前面初始化設(shè)備驅(qū)動的時候,已經(jīng)將 file_operations 指向設(shè)備驅(qū)動的 file_operations 了。這里調(diào)用的是設(shè)備驅(qū)動的 unlocked_ioctl。對于打印機程序來講,調(diào)用的是 lp_ioctl()。

long vfs_ioctl(struct file *filp, unsigned int cmd, unsigned long arg){ int error = -ENOTTY; if (!filp->f_op->unlocked_ioctl) goto out; error = filp->f_op->unlocked_ioctl(filp, cmd, arg); if (error == -ENOIOCTLCMD) error = -ENOTTY; out: return error;} EXPORT_SYMBOL(vfs_ioctl);

??打印機的lp_do_ioctl()主要邏輯也是針對cmd采用switch()語句分情況進行處理。主要包括使用LP_XXX()宏定義賦值標(biāo)記位和調(diào)用copy_to_user()將用戶想得到的信息返回給用戶態(tài)。

static int lp_do_ioctl(unsigned int minor, unsigned int cmd,    unsigned long arg, void __user *argp){    int status;    int retval = 0;......    switch ( cmd ) {        case LPTIME:            if (arg > UINT_MAX / HZ)                return -EINVAL;            LP_TIME(minor) = arg * HZ/100;            break;        case LPCHAR:            LP_CHAR(minor) = arg;            break;        case LPABORT:            if (arg)                LP_F(minor) |= LP_ABORT;            else                LP_F(minor) &= ~LP_ABORT;            break;    ......        case LPGETIRQ:            if (copy_to_user(argp, &LP_IRQ(minor),                    sizeof(int)))                return -EFAULT;            break;......}

總結(jié)

本文簡單介紹了設(shè)備驅(qū)動程序的結(jié)構(gòu),并在此基礎(chǔ)上介紹了字符設(shè)備從創(chuàng)建到打開、寫入以及控制的整個流程。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    日本一品道在线免费观看| 国产在线不卡中文字幕| 国产av一二三区在线观看| 国产不卡视频一区在线| 国产午夜精品亚洲精品国产| 福利专区 久久精品午夜| 日韩人妻毛片中文字幕| 国产精品欧美一区两区| 国产中文字幕久久黄色片| 日本熟妇熟女久久综合| 69精品一区二区蜜桃视频| 黑色丝袜脚足国产一区二区| 国产又粗又猛又爽色噜噜| 亚洲精选91福利在线观看| 高清亚洲精品中文字幕乱码| 国产一区二区三区四区中文| 黄片免费播放一区二区| 亚洲另类女同一二三区| 欧美一级不卡视频在线观看| 日本人妻中出在线观看| 日韩一区二区三区高清在| 污污黄黄的成年亚洲毛片 | 国产日韩精品欧美综合区| 国产又粗又硬又长又爽的剧情| 日韩国产精品激情一区| 精品国模一区二区三区欧美| 国产又粗又长又爽又猛的视频| 一区二区免费视频中文乱码国产| 亚洲欧洲日韩综合二区| 九九热精彩视频在线播放| 日本成人三级在线播放| 午夜直播免费福利平台| 免费观看在线午夜视频| 亚洲中文字幕有码在线观看| 亚洲国产天堂av成人在线播放| 91后入中出内射在线| 千仞雪下面好爽好紧好湿全文 | 少妇视频一区二区三区| 99久久免费看国产精品| 欧美多人疯狂性战派对| 一个人的久久精彩视频|