INPUT输入子系统——按键

一、什么是input输入子系统?

    1.1. Linux系统支持的输入设备繁多,例如键盘、鼠标、触摸屏、手柄或者是一些输入设备像体感输入等等,Linux系统是如何管理如此之多的不同类型、不同原理、不同的输入信息的输入设备的呢?其实就是通过input输入子系统这套软件体系来完成的。从整体上来说,input输入子系统分为3层:上层(输入事件驱动层)、中层(输入核心层)、下层(输入设备驱动层),如下图所示:

    1.2. 图中Drivers对应的就是下层设备驱动层,对应各种各样不同的输入设备,Input Core对应的就是中层核心层,Handlers对应的就是上层输入事件驱动层,最右边的代表的是用户空间

    1.3. 上层中的各个handler(Keyboard/Mouse/Joystick/Event)是属于平行关系。由于历史原因,一种设备可能连接到多个handler层中,由于event是后出的。所有event实现大一统,所有输入设备都可以连接到event handler中

    1.4. 官方查看文档

            文档1:kernel\Documentation\input\input.txt

            文档2:kernel\Documentation\input\input-programming.txt

二. 输入子系统框架分析

    2.1. 重要结构体

        2.1.1.  input_dev结构体

            a. 该结构体是所有输入设备的抽象,只要是输入设备都可以用此结构体描述

struct input_dev {
    const char *name;             //  input设备的名字
    const char *phys;              //  
    const char *uniq;              //
    struct input_id id;             //  

//  这些是用来表示该input设备能够上报的事件类型有哪些   是用位的方式来表示的
    unsigned long evbit[BITS_TO_LONGS(EV_CNT)];
    unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];
    unsigned long relbit[BITS_TO_LONGS(REL_CNT)];
    unsigned long absbit[BITS_TO_LONGS(ABS_CNT)];
    unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)];
    unsigned long ledbit[BITS_TO_LONGS(LED_CNT)];
    unsigned long sndbit[BITS_TO_LONGS(SND_CNT)];
    unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];
    unsigned long swbit[BITS_TO_LONGS(SW_CNT)];

    unsigned int keycodemax;
    unsigned int keycodesize;
    void *keycode;
    int (*setkeycode)(struct input_dev *dev,
              unsigned int scancode, unsigned int keycode);
    int (*getkeycode)(struct input_dev *dev,
              unsigned int scancode, unsigned int *keycode);

    struct ff_device *ff;

    unsigned int repeat_key;
    struct timer_list timer;

    int sync;

    int abs[ABS_CNT];
    int rep[REP_MAX + 1];

    unsigned long key[BITS_TO_LONGS(KEY_CNT)];
    unsigned long led[BITS_TO_LONGS(LED_CNT)];
    unsigned long snd[BITS_TO_LONGS(SND_CNT)];
    unsigned long sw[BITS_TO_LONGS(SW_CNT)];

    int absmax[ABS_CNT];
    int absmin[ABS_CNT];
    int absfuzz[ABS_CNT];
    int absflat[ABS_CNT];
    int absres[ABS_CNT];

    int (*open)(struct input_dev *dev);              //    设备的open函数
    void (*close)(struct input_dev *dev);          
    int (*flush)(struct input_dev *dev, struct file *file);
    int (*event)(struct input_dev *dev, unsigned int type, unsigned int code, int value);     //  上报事件

    struct input_handle *grab;

    spinlock_t event_lock;
    struct mutex mutex;

    unsigned int users;
    bool going_away;

    struct device dev;                 //  内置的device结构体变量

    struct list_head    h_list;    //  用来挂接input_dev 设备连接的所有handle 的一个链表头
    struct list_head    node;    //  作为链表节点挂接到  input_dev_list 链表上  (input_dev_list链表是input核心层维护的一个用来挂接所有input设备的一个链表头)
};
View Code

            2.1.2. input_handler结构体

struct input_handler {

    void *private;            //  私有数据

    void (*event)(struct input_handle *handle, unsigned int type, unsigned int code, int value);   //  handler用于向上层上报输入事件的函数
    bool (*filter)(struct input_handle *handle, unsigned int type, unsigned int code, int value);
    bool (*match)(struct input_handler *handler, struct input_dev *dev);            //   match 函数用来匹配handler 与 input_dev 设备
    int (*connect)(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id);   //  当handler 与 input_dev 匹配成功之后用来连接
    void (*disconnect)(struct input_handle *handle);          //  断开handler 与 input_dev 之间的连接
    void (*start)(struct input_handle *handle);                    

    const struct file_operations *fops;             //  一个file_operations 指针
    int minor;                                      //  该handler 的编号 (在input_table 数组中用来计算数组下标) input_table数组就是input子系统用来管理注册的handler的一个数据结构
    const char *name;                               //  handler的名字

    const struct input_device_id *id_table;      //  指向一个 input_device_id  类型的数组,用来进行与input设备匹配时用到的信息
 
    struct list_head    h_list;       //  用来挂接handler 上连接的所有handle 的一个链表头
    struct list_head    node;        //  作为一个链表节点挂接到 input_handler_list 链表上(input_handler_list 链表是一个由上层handler参维护的一个用来挂接所有注册的handler的链表头)
};
View Code

            2.1.3. input_device_id结构体

struct input_device_id {

    kernel_ulong_t flags;    //  这个flag 表示我们的这个 input_device_id 是用来匹配下面的4个情况的哪一项
                                    //  flag == 1表示匹配总线  2表示匹配供应商   4表示匹配产品  8表示匹配版本
    __u16 bustype;
    __u16 vendor;
    __u16 product;
    __u16 version;

    kernel_ulong_t evbit[INPUT_DEVICE_ID_EV_MAX / BITS_PER_LONG + 1];
    kernel_ulong_t keybit[INPUT_DEVICE_ID_KEY_MAX / BITS_PER_LONG + 1];
    kernel_ulong_t relbit[INPUT_DEVICE_ID_REL_MAX / BITS_PER_LONG + 1];
    kernel_ulong_t absbit[INPUT_DEVICE_ID_ABS_MAX / BITS_PER_LONG + 1];
    kernel_ulong_t mscbit[INPUT_DEVICE_ID_MSC_MAX / BITS_PER_LONG + 1];
    kernel_ulong_t ledbit[INPUT_DEVICE_ID_LED_MAX / BITS_PER_LONG + 1];
    kernel_ulong_t sndbit[INPUT_DEVICE_ID_SND_MAX / BITS_PER_LONG + 1];
    kernel_ulong_t ffbit[INPUT_DEVICE_ID_FF_MAX / BITS_PER_LONG + 1];
    kernel_ulong_t swbit[INPUT_DEVICE_ID_SW_MAX / BITS_PER_LONG + 1];

    kernel_ulong_t driver_info;
};
View Code

    2.2. input框架中重要函数

        2.2.1、核心模块注册input_init

            a. 函数位于drivers\input\input.c

            b. class_register

            c. input_proc_init

            d. register_chrdev

static int __init input_init(void)
{
    int err;

    input_init_abs_bypass();

    err = class_register(&input_class);                //  创建设备类    /sys/class/input
    if (err) {
        printk(KERN_ERR "input: unable to register input_dev class\n");
        return err;
    }

    err = input_proc_init();           //    proc文件系统相关的初始化
    if (err)
        goto fail1;

    err = register_chrdev(INPUT_MAJOR, "input", &input_fops);       //   注册字符设备驱动   主设备号13   input_fops 中只实现了open函数,所以他的原理其实和misc其实是一样的
    if (err) {
        printk(KERN_ERR "input: unable to register char major %d", INPUT_MAJOR);
        goto fail2;
    }

    return 0;

 fail2:    input_proc_exit();
 fail1:    class_unregister(&input_class);
    return err;
}
View Code

        2.2.2. 核心层提供给设备驱动层的接口函数

            2.2.2.1. input设备驱动框架留给设备驱动层的接口函数主要有3个: 

                a. input_allocate_device。分配一块input_dev结构体类型大小的内存 

struct input_dev *input_allocate_device(void)
{
    struct input_dev *dev;                 //   定义一个 input_dev  指针

    dev = kzalloc(sizeof(struct input_dev), GFP_KERNEL);   //  申请分配内存
    if (dev) {
        dev->dev.type = &input_dev_type;          //  确定input设备的 设备类型     input_dev_type
        dev->dev.class = &input_class;                //  确定input设备所属的设备类   class
        device_initialize(&dev->dev);                   //  input设备的初始化
        mutex_init(&dev->mutex);                        //  互斥锁初始化
        spin_lock_init(&dev->event_lock);            //  自旋锁初始化
        INIT_LIST_HEAD(&dev->h_list);                 //  input_dev -> h_list 链表初始化
        INIT_LIST_HEAD(&dev->node);                 //  input_dev -> node 链表初始化

        __module_get(THIS_MODULE);
    }

    return dev;
}
View Code

                b. input_set_capability。设置输入设备可以上报哪些输入事件 

                    函数原型:input_set_capability(struct input_dev *dev, unsigned int type, unsigned int code) 

                        参数:dev就是设备的input_dev结构体变量

                                  type表示设备可以上报的事件类型 

                                  code表示上报这类事件中的那个事件 

                                  注意:input_set_capability函数一次只能设置一个具体事件,如果设备可以上报多个事件,则需要重复调用这个函数来进行设置,

                    例如:  input_set_capability(dev, EV_KEY, KEY_Q); // 至于函数内部是怎么设置的,将会在后面进行分析。 

                                input_set_capability(dev, EV_KEY, KEY_W); 

                                input_set_capability(dev, EV_KEY, KEY_E);

                c. input_register_device。向input核心层注册设备

int input_register_device(struct input_dev *dev)      //  注册input输入设备
{
    static atomic_t input_no = ATOMIC_INIT(0);
    struct input_handler *handler;                          //  定义一个  input_handler 结构体指针
    const char *path;
    int error;

    /* Every input device generates EV_SYN/SYN_REPORT events. */
    __set_bit(EV_SYN, dev->evbit);                  //   每一个input输入设备都会发生这个事件

    /* KEY_RESERVED is not supposed to be transmitted to userspace. */
    __clear_bit(KEY_RESERVED, dev->keybit);  //  清除KEY_RESERVED 事件对应的bit位,也就是不传输这种类型的事件

    /* Make sure that bitmasks not mentioned in dev->evbit are clean. */
    input_cleanse_bitmasks(dev);           //   确保input_dev中的用来记录事件的变量中没有提到的位掩码是干净的。

    /*
     * If delay and period are pre-set by the driver, then autorepeating
     * is handled by the driver itself and we don't do it in input.c.
     */
    init_timer(&dev->timer);
    if (!dev->rep[REP_DELAY] && !dev->rep[REP_PERIOD]) {
        dev->timer.data = (long) dev;
        dev->timer.function = input_repeat_key;
        dev->rep[REP_DELAY] = 250;
        dev->rep[REP_PERIOD] = 33;
    }

    if (!dev->getkeycode)
        dev->getkeycode = input_default_getkeycode;

    if (!dev->setkeycode)
        dev->setkeycode = input_default_setkeycode;

    dev_set_name(&dev->dev, "input%ld",                                  //   设置input设备对象的名字    input+数字
             (unsigned long) atomic_inc_return(&input_no) - 1);

    error = device_add(&dev->dev);         //   添加设备       例如:          /sys/devices/virtual/input/input0     
    if (error)
        return error;

    path = kobject_get_path(&dev->dev.kobj, GFP_KERNEL);  //  获取input设备对象所在的路径      /sys/devices/virtual/input/input_xxx   
    printk(KERN_INFO "input: %s as %s\n",
        dev->name ? dev->name : "Unspecified device", path ? path : "N/A");
    kfree(path);

    error = mutex_lock_interruptible(&input_mutex);
    if (error) {
        device_del(&dev->dev);
        return error;
    }

    list_add_tail(&dev->node, &input_dev_list);             //   链表挂接:    将 input_dev->node 作为节点挂接到 input_dev_list  链表上

    list_for_each_entry(handler, &input_handler_list, node)  //  遍历input_handler_list 链表上的所有handler
        input_attach_handler(dev, handler);                        //  将handler与input设备进行匹配

    input_wakeup_procfs_readers();                //  更新proc 文件系统

    mutex_unlock(&input_mutex);

    return 0;
}
View Code

                d. input_attach_handler函数:

                    input_attach_handler就是input_register_device函数中用来对下层的设备驱动和上层的handler进行匹配的一个函数,只有匹配成功之后就会调用上层handler中的connect函数

 

static int input_attach_handler(struct input_dev *dev, struct input_handler *handler)
{
    const struct input_device_id *id;                //   定义一个input_device_id 的指针
    int error;

    id = input_match_device(handler, dev);   //  通过这个函数进行handler与input设备的匹配工作
    if (!id)
        return -ENODEV;

    error = handler->connect(handler, dev, id);  //  匹配成功则调用 handler 中的 connect 函数进行连接
    if (error && error != -ENODEV)
        printk(KERN_ERR
            "input: failed to attach handler %s to device %s, "
            "error: %d\n",
            handler->name, kobject_name(&dev->dev.kobj), error);

    return error;
}



static const struct input_device_id *input_match_device(struct input_handler *handler,
                            struct input_dev *dev)
{
    const struct input_device_id *id;            //   定义一个 input_device_id  指针
    int i;

    for (id = handler->id_table; id->flags || id->driver_info; id++) {  //  依次遍历handler->id_table 所指向的input_device_id 数组中的各个元素
                                                                                                                    //  依次进行下面的匹配过程
        if (id->flags & INPUT_DEVICE_ID_MATCH_BUS)         //    匹配总线
            if (id->bustype != dev->id.bustype)
                continue;

        if (id->flags & INPUT_DEVICE_ID_MATCH_VENDOR)  //  匹配供应商
            if (id->vendor != dev->id.vendor)
                continue;

        if (id->flags & INPUT_DEVICE_ID_MATCH_PRODUCT)  //  匹配产品
            if (id->product != dev->id.product)
                continue;

        if (id->flags & INPUT_DEVICE_ID_MATCH_VERSION)  //  匹配版本
            if (id->version != dev->id.version)
                continue;

    //    下面的这些是匹配我们上传的事件是否属实
        MATCH_BIT(evbit,  EV_MAX);
        MATCH_BIT(keybit, KEY_MAX);
        MATCH_BIT(relbit, REL_MAX);
        MATCH_BIT(absbit, ABS_MAX);
        MATCH_BIT(mscbit, MSC_MAX);
        MATCH_BIT(ledbit, LED_MAX);
        MATCH_BIT(sndbit, SND_MAX);
        MATCH_BIT(ffbit,  FF_MAX);
        MATCH_BIT(swbit,  SW_MAX);

        if (!handler->match || handler->match(handler, dev))
            return id;        //    如果数组中的某个匹配成功了就返回他的地址
    }

    return NULL;
}
View Code

三. 驱动开发者开发流程

    3.1. 内核提供的函数

        3.1.1. input_allocate_device

        3.1.2. input_register_device

        3.1.3. input_register_device

    3.2. 开发代码

        3.2.1. button_device.c

#include <linux/init.h>            // __init   __exit
#include <linux/module.h>      // module_init  module_exit
#include <mach/regs-gpio.h>
#include <mach/gpio-bank.h>

#include <mach/gpio.h> 
#include <linux/leds.h>
#include <asm/string.h> 

#include <linux/platform_device.h>

#include <linux/input.h> 
#include "button_device_driver.h"

#define BUTTON_GPIO_CONFIG 0xFF

/* KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT
    * LEFT     -> EINT2    -> GPH0_2
    * DOWN     -> EINT3    -> GPH0_3
    * UP     -> KP_COL0 -> GPH2_0
    * RIGHT  -> KP_COL1 -> GPH2_1
    * MENU     -> KP_COL3 -> GPH2_3 (KEY_A)
    * BACK     -> KP_COL2 -> GPH2_2 (KEY_B)
    */

void s5pv210_button_release(struct device *dev);

struct gpioIRQ button_irq[] = 
{
    {
        "button_Left_IRQ",
        BUTTON_LEFT_IRQ,
        IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING,
    },
    {
        "button_down_IRQ",
        BUTTON_DOWN_IRQ,
        IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING,
    }
};
static struct s5pv210_button_platdata x210_button_pdata[] = {
    [0]={
        .name        = "button_Left",
        .gpio        = S5PV210_GPH0(2),
        .gpio_cfg    = BUTTON_GPIO_CONFIG,
        .button_code= KEY_LEFT,
        .button_irq    = &button_irq[0],
        },
    [1]={
        .name        = "button_down",
        .gpio        =  S5PV210_GPH0(3),
        .gpio_cfg    = BUTTON_GPIO_CONFIG,
        .button_code= KEY_DOWN,
        .button_irq    = &button_irq[1],
        }
};


static struct platform_device s5pv210_device_button = {
    .name    = "s5pv210-button",
    .id        = -1,
    .dev    = {
            .platform_data    = &x210_button_pdata,
            .release = s5pv210_button_release,
        },
};
void s5pv210_button_release(struct device *dev)
{
    printk(KERN_WARNING "s5pv210_button_release successful \n");
}

static int __init s5pv210_button_init(void)
{
    int ret = -1;
    printk(KERN_INFO "device.c : s5pv210_button_init successful \n");    
    ret = platform_device_register(&s5pv210_device_button);
    if (ret < 0) 
    {
        printk(KERN_WARNING "platform_add_devices fail \n");
    }
    return ret;
}

static void __exit s5pv210_button_exit(void)
{    
    printk(KERN_INFO "device.c : s5pv210_button_exit successful \n");

    platform_device_unregister(&s5pv210_device_button); 
}




module_init(s5pv210_button_init);
module_exit(s5pv210_button_exit);

// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");                // 描述模块的许可证
MODULE_AUTHOR("musk");                // 描述模块的作者
MODULE_DESCRIPTION("x210 button device");    // 描述模块的介绍信息
MODULE_ALIAS("button_device");            // 描述模块的别名信息
        3.2.2. button_driver.c

#include <linux/input.h> 
#include <linux/module.h> 
#include <linux/init.h>
#include <asm/irq.h> 
#include <asm/io.h>

#include <mach/irqs.h>

#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>

#include "button_device_driver.h"
#include <linux/kernel.h> 
/*
 * POWER  -> EINT1   -> GPH0_1
 * LEFT   -> EINT2   -> GPH0_2
 * DOWN   -> EINT3   -> GPH0_3
 * UP     -> KP_COL0 -> GPH2_0
 * RIGHT  -> KP_COL1 -> GPH2_1
 * MENU   -> KP_COL3 -> GPH2_3 (KEY_A)
 * BACK   -> KP_COL2 -> GPH2_2 (KEY_B)
 */

static struct input_dev *button_dev;


static irqreturn_t button_interrupt(int irq, void *dummy) 
{ 
    
    struct platform_device *pdev = (struct platform_device *)dummy;
    struct s5pv210_button_platdata *pdata = pdev->dev.platform_data;
    if(dummy == NULL)
    {
        printk("\n button_interrupt fail;irq = %d\n",irq);
        return -1;
    }
    printk("\n irq = %d\n",irq);
    if(irq == pdata[0].button_irq->irq)
    {
        s3c_gpio_cfgpin(pdata[0].gpio, S3C_GPIO_SFN(0x00));
        input_report_key(button_dev, pdata[0].button_code,!gpio_get_value(pdata[0].gpio));
        s3c_gpio_cfgpin(pdata[0].gpio, S3C_GPIO_SFN(0x0f));
    }
    if(irq == pdata[1].button_irq->irq)
    {
        s3c_gpio_cfgpin(pdata[1].gpio, S3C_GPIO_SFN(0x00));
        input_report_key(button_dev, pdata[1].button_code,!gpio_get_value(pdata[1].gpio));
        s3c_gpio_cfgpin(pdata[1].gpio, S3C_GPIO_SFN(0x0f));
    }
    input_sync(button_dev);

    return IRQ_HANDLED; 
}


static int s5pv210_button_probe(struct platform_device *pdev)
{
    int error,ret;
    struct s5pv210_button_platdata *pdata = pdev->dev.platform_data;
    printk(KERN_INFO "button_driver.c: s5pv210_button_probe successful\n");
    if (request_irq(pdata[0].button_irq->irq, button_interrupt, pdata[0].button_irq->irq_trigger, pdata[0].button_irq->name, pdev)) 
    { 
        printk(KERN_ERR "key-s5pv210.c: Can't allocate irq %d\n", pdata[0].button_irq->irq);
        return -EBUSY;
    }
    ret = gpio_request(pdata[0].gpio, pdata[0].name);
    if(ret)
        printk("button-driver: request %s fail", pdata[0].name);
    s3c_gpio_cfgpin(pdata[0].gpio, S3C_GPIO_SFN(pdata[0].gpio_cfg));
    
    
    
    if (request_irq(pdata[1].button_irq->irq, button_interrupt, pdata[1].button_irq->irq_trigger, pdata[1].button_irq->name, pdev)) 
    { 
        printk(KERN_ERR "key-s5pv210.c: Can't allocate irq %d\n", pdata[1].button_irq->irq);
        return -EBUSY;
    }
        ret = gpio_request(pdata[1].gpio, pdata[1].name);
    if(ret)
        printk(KERN_ERR "button-driver: request %s fail", pdata[1].name);
    s3c_gpio_cfgpin(pdata[1].gpio, S3C_GPIO_SFN(pdata[1].gpio_cfg));
    
    
    button_dev = input_allocate_device();
    if (!button_dev)    
    { 
        printk(KERN_ERR "button.c: Not enough memory\n");
        error = -ENOMEM;
        goto err_free_irq; 
    }
    
    set_bit(EV_KEY, button_dev->evbit);

    set_bit(pdata[0].button_code, button_dev->keybit);
    set_bit(pdata[1].button_code, button_dev->keybit);

    error = input_register_device(button_dev);
    if (error) 
    { 
        printk(KERN_ERR "button_driver.c: Failed to register device\n");
        goto err_free_dev; 
    }
    return 0;
 
 err_free_dev:
    input_free_device(button_dev);
    
 err_free_irq:
    free_irq(pdata[0].button_irq->irq, pdev);
    free_irq(pdata[1].button_irq->irq, pdev);
    gpio_free(pdata[0].gpio);
    gpio_free(pdata[1].gpio);
    
    return error;
}

static int s5pv210_button_remove(struct platform_device *pdev)
{
    struct s5pv210_button_platdata *pdata = pdev->dev.platform_data;
    printk(KERN_INFO "button_driver.c: s5pv210_button_remove successful\n");
    
    free_irq(IRQ_EINT2, pdev);
    free_irq(IRQ_EINT3, pdev);
    gpio_free(pdata[0].gpio);    
    gpio_free(pdata[1].gpio);    
    input_unregister_device(button_dev);

    
    return  0;
}

static struct platform_driver s5pv210_button_driver = {
    .probe        = s5pv210_button_probe,
    .remove        = s5pv210_button_remove,
    .driver        = {
        .name    = "s5pv210-button",
        .owner    = THIS_MODULE,
    }
};

static int __init s5pv210_button_init(void) 
{ 
    return platform_driver_register(&s5pv210_button_driver);
}
static void __exit s5pv210_button_exit(void)
{
    platform_driver_unregister(&s5pv210_button_driver);
}

module_init(s5pv210_button_init); 
module_exit(s5pv210_button_exit);

MODULE_AUTHOR("musk");
MODULE_DESCRIPTION("s5pv210 button driver");
MODULE_LICENSE("GPL");
MODULE_ALIAS("button-s5pv210");
        3.2.3. button_device_driver.h

#ifndef __BUTTON_DEVICE_DRIVER_H
#define __BUTTON_DEVICE_DRIVER_H 

#define BUTTON_LEFT_IRQ  IRQ_EINT2
#define BUTTON_DOWN_IRQ  IRQ_EINT3

#include <mach/irqs.h>
#include <linux/interrupt.h>

struct gpioIRQ {
    char            *name;
    unsigned int     irq;
    unsigned long    irq_trigger;

};
struct s5pv210_button_platdata {
    char            *name;

    unsigned int    gpio;
    unsigned int     gpio_cfg;
    unsigned int    button_code;
    struct gpioIRQ     *button_irq;
};

#endif /* __ASM_ARCH_LEDSGPIO_H */
View Code

 

索引文献:https://www.cnblogs.com/deng-tao/p/6094049.html

posted @ 2019-01-03 09:19  三七鸽  阅读(3028)  评论(0编辑  收藏  举报