(3.6)一個按鍵所能涉及的:輸入子系統

/* AUTHOR: Pinus

* Creat on : 2018-10-28

* KERNEL : linux-4.4.145

* BOARD : JZ2440(arm9 s3c2440)

* REFS : 韋東山視頻教程第二期

               linux驅動子系統之輸入子系統(1)

               我對linux驅動 輸入子系統的理解

               Linux之輸入子系統分析(詳解)

*/

概述

前文自己的按鍵驅動的時候:

        實現模塊init(入口)、exit(出口)函數,設置或自動生成設備主設備號,註冊設備類、設備節點,實現file_operation結構具體操作。

        若有多個不同的驅動程序時,應用程序就要打開多個不同的驅動設備,如果僅僅是自己看或者使用當然會很清楚怎麼使用修改,但實際上我們的代碼是要給別人看或者使用的,別人來使用時就會很麻煩。因此Linux引入input(輸入子系統), 使應用程序無需打開多個不同的驅動設備便能實現

什麼是輸入子系統?

        linux內核中自帶了很多的驅動子系統,其中比較典型的有:input子系統,led子系統,framebuffer子系統(LCD), I2c子系統等,這些子系統它是通過一層一層的函數傳遞與封裝,它實現了設備驅動的註冊,以及定義了file-operations結構體裏面的各種函數操作等,不需要在單獨的設備驅動代碼中進行註冊,定義,直接調用相應的的子系統即可,內核提供的輸入子系統是對分散的、多種不同類別的輸入設備(鍵盤、鼠標、觸摸屏、加速計、跟蹤球、操縱桿等)進行統一處理的驅動程序。

輸入子系統帶來的好處:

抽象底層形態各異的硬件輸入設備,爲上層提供了統一的操作接口

提高了代碼重用率和效率,減少了bug

輸入子系統框架:

輸入子系統的三層結構:

l  事件驅動層

         負責和應用程序的接口

l  核心層

         提供事件驅動層和設備驅動層所需的函數接口

l  設備驅動層

         負責和底層輸入設備通信

事件驅動層和核心層都是通用的,我們需要實現的是設備驅動層。輸入設備驅動會把硬件產生的事件信息用統一的格式(struct input_event)上報給核心層,然後核心層進行分類後,再上報給相應的事件處理驅動程序,最後通過事件層傳遞到用戶空間,應用程序可以通過設備節點來獲取事件信息。比如底層報上來的是一個按鍵事件,核心層會報給evdev來處理;如果報的是一個鼠標事件,核心層會報給mousedev來處理。

分析輸入子系統

drivers/input/input.c 核心層

subsys_initcall(input_init); //input子系統的實現是類似模塊的方法

module_exit(input_exit);

從input_init,分析【註冊流程】

static LIST_HEAD(input_dev_list);              // 定義全局鏈表 : input device

static LIST_HEAD(input_handler_list);       // 定義全局鏈表 : input handler

 

input_init()      //入口函數

    |

    class_register(&input_class)     // 將class註冊到內核中,同時創建/sys/class/下節點,類似class_create()

    input_proc_init()         // 創建 /proc下對應文件,導出設備信息到proc, 沒啥用

        |

        proc_bus_input_dir = proc_mkdir("bus/input", NULL); // 創建文件夾"/proc/bus/input"

        // 創建節點"/proc/bus/input/devices"

        entry = proc_create("devices", 0, proc_bus_input_dir, &input_devices_fileops);

        // 創建節點"/proc/bus/input/handlers"

        entry = proc_create("handlers", 0, proc_bus_input_dir, &input_handlers_fileops);

        // 根據要求申請主設備號,主設備號INPUT_MAJOR == 13,這裏只是申請了區域但還沒有創建具體設備節點,屬於創建設備的一步,這種創建設備的方式你前面也提過。

   register_chrdev_region(MKDEV(INPUT_MAJOR, 0), INPUT_MAX_CHAR_DEVICES, "input");

 

input handler層: evdev.c 【註冊流程】

evdev_init()     // module_init(evdev_init)

    |

    input_register_handler(&evdev_handler)

static struct input_handler evdev_handler = {
    .event        = evdev_event,
    .events       = evdev_events,
    .connect      = evdev_connect,
    .disconnect   = evdev_disconnect,
    .legacy_minors = true,
    .minor        = EVDEV_MINOR_BASE,
    .name         = "evdev",
    .id_table     = evdev_ids,
};

        |

        INIT_LIST_HEAD(&handler->h_list);   //初始化鏈表頭,把鏈表的前和後都指向它自己

        list_add_tail(&handler->node, &input_handler_list);  // 把 handler的 node 加到 input_handler_list這個雙向鏈表

        list_for_each_entry(dev, &input_dev_list, node)    // 遍歷input_dev_list鏈表,鏈表中每一個input device均嘗試與當前的input handler(evdev)匹配

        input_attach_handler(dev, handler); // evdev可以與任何input device匹配,因爲evdev的id_table[]爲空

            |

            id = input_match_device(handler, dev); // 根據evdev的id_table[]進行匹配 - 滿足id_table[]中的全部條件才能匹配成功

            error = handler->connect(handler, dev, id); // 匹配成功後調用handler中connect() -- .cevdev_connect,

        input_wakeup_procfs_readers(); // 將當前的handler加入到/proc/bus/input/handlers文件中

總結:

1. 註冊了evdev_handler

2. 遍歷input_dev_list,進行行匹配,匹配成功,調用handler中connect方法--- evdev_connect()

3. 內核有好幾個input handler: evdev、mousedev、joydev、evbug等

4. 其中 evdev 可以處理所有的事件,觸摸屏驅動,sensor就是用的這個。

 

input 驅動層: button.c 【註冊流程】 示例解析

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/sched.h>
#include <linux/pm.h>
#include <linux/slab.h>
#include <linux/sysctl.h>
#include <linux/proc_fs.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/input.h>
#include <linux/gpio_keys.h>
#include <linux/workqueue.h>
#include <linux/gpio.h>
#include <linux/of.h>
#include <linux/of_platform.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/spinlock.h>

#include <asm/io.h>
#include <asm/irq.h>
#include <asm/uaccess.h>

#include <mach/regs-gpio.h>
#include <mach/gpio-samsung.h>

static struct input_dev *buttons_dev;  //聲明一個輸入子系統設備

struct jzpin_desc{
	int irq;
	char *name;
	unsigned int pin;
	unsigned int key_val;
};

struct jzpin_desc pins_desc[4] = {
	{IRQ_EINT0,  "S2", S3C2410_GPF(0),  KEY_L},
	{IRQ_EINT2,  "S3", S3C2410_GPF(2),  KEY_S},
	{IRQ_EINT11, "S4", S3C2410_GPG(3),  KEY_ENTER},
	{IRQ_EINT19, "S5", S3C2410_GPG(11), KEY_LEFTSHIFT},
};
static struct jzpin_desc *irq_pd;

static struct timer_list button_timer;  //定時器防抖動

static irqreturn_t button_irq_handle(int irq, void *dev_id)
{
	irq_pd = (struct intpin_desc *)dev_id;
	/* 10ms後啓動定時器 */
	mod_timer(&button_timer, jiffies + HZ/100);
	return IRQ_HANDLED;
}

/* Handle the timer event */
static void button_expire_timeout(unsigned long unused)
{
	struct jzpin_desc * pindesc = irq_pd;
	unsigned int pinval;

	if (!pindesc)
			return 0;
	
	pinval = gpio_get_value(pindesc->pin);

        /*上報事件*/
	if (pinval){
		/*鬆開 : 最後一個參數表示:0-鬆開, 1-按下*/
		input_event(buttons_dev, EV_KEY, pindesc->key_val, 0);
		input_sync(buttons_dev);  //同步事件,表示上報完
	}else{
		/*按下*/
		input_event(buttons_dev, EV_KEY, pindesc->key_val, 1);
		input_sync(buttons_dev);
	}

}

static int __init buttons_init(void)
{
	int i;
	
	/* 1. 分配一個input_dev結構體 */
	buttons_dev = input_allocate_device();
	/* 2. 設置 */
		//2.1 產生哪一類事件
		//2.2 產生哪一些事件 L,S,ENTER,LEFTSHIFT
	__set_bit(EV_REP, buttons_dev->evbit);
	input_set_capability(buttons_dev, EV_KEY, KEY_L);
	input_set_capability(buttons_dev, EV_KEY, KEY_S);
	input_set_capability(buttons_dev, EV_KEY, KEY_ENTER);
	input_set_capability(buttons_dev, EV_KEY, KEY_LEFTSHIFT);
	/* 3.註冊 */
	input_register_device(buttons_dev);
	/* 4.硬件相關操作 */
	init_timer(&button_timer);
	setup_timer(&button_timer, button_expire_timeout, 0);
	add_timer(&button_timer);
	
	for (i=0; i<4; i++){
		request_irq(pins_desc[i].irq, 	button_irq_handle, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, pins_desc[i].name, &pins_desc[i]);  //註冊中斷
	}
	
	return 0;
}

static void __exit buttons_exit(void)
{
	int i;
	for (i=0; i<4; i++){
		free_irq(pins_desc[i].irq, &pins_desc[i]);
	}
	del_timer(&button_timer);
	input_unregister_device(buttons_dev);
	input_free_device(buttons_dev);
} 

module_init(buttons_init);
module_exit(buttons_exit);

MODULE_AUTHOR("[email protected]");
MODULE_LICENSE("GPL");

分析【註冊流程】

input_register_device(buttons_dev);

    |

    error = device_add(&dev->dev);    // 創建設備節點

    list_add_tail(&dev->node, &input_dev_list); // 重要,把input device掛到全局的鏈表input_dev_list上

    // 核心重點,遍歷input_handler_list鏈表,鏈表中每一個input handler均嘗試與當前的input device匹配

    list_for_each_entry(handler, &input_handler_list, node)

    input_attach_handler(dev, handler); // evdev可以與任何input device匹配,適應所有設備

        |

        id = input_match_device(handler, dev); // 根據evdev的id_table[]進行匹配 - 滿足id_table[]中的全部條件才能匹配成功

        error = handler->connect(handler, dev, id); // 匹配成功後調用handler中connect() -- evdev_connect

當input_dev和input_handler匹配成功

evdev_connect(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id)

    |

    // 分配次設備號,找到一個尚未被使用的最小次設備號,從64開始,65,66

    minor = input_get_new_minor(EVDEV_MINOR_BASE, EVDEV_MINORS, true);

    evdev = kzalloc(sizeof(struct evdev), GFP_KERNEL); // 實例化一個evdev對象

    INIT_LIST_HEAD(&evdev->client_list); // 多個應用打開同一個device時,每次open都生成一個clinet,掛載到client_list,數據上報時遍歷鏈表,copy到所有成員的buffer中

    init_waitqueue_head(&evdev->wait);  // 等待隊列用於完成阻塞,read()的時候,沒數據(緩存隊列頭等於尾)就睡眠,喚醒條件爲有數據(緩存隊列頭不等於尾),input_sync()顯示喚醒

    ====

    等待隊列的實現:

    1 wait_queue_head_t mywq_head;

    2 init_waitqueue_head(&mywq_head);

    3 wait_event_interruptible( mywq_head, fs210_btn_device->btn_state); // 條件不滿足,就把調用進程掛起

    4 wake_up_interruptible(&mywq_head); // 喚醒等待隊列 - 等待隊列需要顯式喚醒

    ====

    dev_set_name(&evdev->dev, "event%d", dev_no); // 創建設備文件/dev/input/event0/1/2 以下代碼與device_create()一樣

    evdev->dev.devt = MKDEV(INPUT_MAJOR, minor);

    evdev->dev.class = &input_class;

    evdev->dev.parent = &dev->dev;

    evdev->dev.release = evdev_free;

    device_initialize(&evdev->dev);

    evdev->handle.dev = input_get_device(dev); // 利用handle記錄input device和input handler(經過匹配後的)

    evdev->handle.handler = handler;

    evdev->handle.private = evdev; // 後面evdev_events(struct input_handle *handle,)根據handle 拿到evdev

    error = input_register_handle(&evdev->handle);  // 將handle與input device關聯,互相可以找到

        |

        list_add_tail_rcu(&handle->d_node, &dev->h_list); // 將handle與input device關聯,互相可以找到

        list_add_tail_rcu(&handle->h_node, &handler->h_list); // 將handle與input handler關聯,互相可以找到

    cdev_init(&evdev->cdev, &evdev_fops); // 初始化並註冊字符設備cdev,完成fops,爲用戶提供文件io接口

    cdev_add(&evdev->cdev, evdev->dev.devt, 1);

總結:

1. 分配evdev,並初始化,創建handle 記錄 相匹配的input device和input handler的節點

2. 創建設備節點/dev/input/event0/1/2

3. 註冊cdev,並實現fops

4. 關係:

    多個input device可以對應一個input handler

    一個input device對應一個evdev,對應一個設備節點:/dev/input/event0/1/2

    一個input device可以被多個應用打開,每次打開生成一個clinet,掛載到evdev中的client_list鏈表

5. 所有設備節點被調用open(),read(),write()文件io的時候

    實際上都是調用cdev中fops的各個接口:

static const struct file_operations evdev_fops = {

    .owner = THIS_MODULE,
    .read = evdev_read,
    .write = evdev_write,
    .poll = evdev_poll,
    .open = evdev_open,
    ...
};

 

按鍵採用定時器延時,所以上報在定時器處理函數中

/* Handle the timer event */
static void button_expire_timeout(unsigned long unused)
{
    struct jzpin_desc * pindesc = irq_pd;
    unsigned int pinval;
 
    if (!pindesc)
        return 0;

    pinval = gpio_get_value(pindesc->pin);
    if (pinval){
        /*鬆開 : 最後一個參數表示:0-鬆開, 1-按下*/
        input_event(buttons_dev, EV_KEY, pindesc->key_val, 0);
        input_sync(buttons_dev); //同步事件,表示上報完
    }else{
        /*按下*/
        input_event(buttons_dev, EV_KEY, pindesc->key_val, 1);
        input_sync(buttons_dev);
    }
}

底層驅動可以通過input_event上報事件信息,上層應用也可以通過此結構體來獲取事件信息

struct input_event {

         structtimeval time;   /* 事件產生時間 */

         __u16type;               /* 事件類型 */

         __u16code;              /* 事件代碼 */

         __s32value;             /* 事件值 */

};

n  報告輸入事件

// 報告按鍵值

voidinput_report_key(struct input_dev *dev, unsigned int code, int value)

// 報告相對座標

voidinput_report_rel(struct input_dev *dev, unsigned int code, int value)

//報告絕對座標

void input_report_abs(struct input_dev *dev, unsigned int code, int value)

//用於事件同步 ,防止數據混亂

void input_sync(struct input_dev *dev)

以上都是對Input_event的封裝

void input_event(struct input_dev *dev,unsigned int type, unsigned int code, intvalue)

實際使用程序流程

1. 應用程序調用open()

根據以前的經驗我們最終會最終調用了evdev_open()

evdev_open(struct inode *inode, struct file *file)

    |

   struct evdev *evdev = container_of(inode->i_cdev, struct evdev, cdev); // inode->i_cdev就是connect()中住的的cdev

    unsigned int bufsize = evdev_compute_buffer_size(evdev->handle.dev); // 通過handle找到 input device,根據input device 獲取緩衝區的大小(幾個input event),但是我們驅動中未給定緩衝區大小,系統會自動給定一個

    unsigned int size = sizeof(struct evdev_client) + bufsize * sizeof(struct input_event);  // size包含了很多個input event

    struct evdev_client *client;

    client = kzalloc(size, GFP_KERNEL | __GFP_NOWARN); // 分配一個client對象,用來描述一個緩衝隊列,存放的就是input_event

    client->bufsize = bufsize; // client中有一個緩衝區

    spin_lock_init(&client->buffer_lock);

    client->evdev = evdev;   // evdev_client中記錄evdev

    evdev_attach_client(evdev, client); // 將client 加入到evdev中的一個小鏈表中

        |

        list_add_tail_rcu(&client->node, &evdev->client_list);

        file->private_data = client;  // evdev_client記錄到file中,方便其他接口調用(這裏是open(),其他接口還有read()、write())

總結:

1. 爲輸入設備分配一個緩衝區evdev_client,用於存放input device層上報的數據

2. evdev_client中記錄evdev

3. evdev_client記錄到file中,方便其他read() write() 等接口使用

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

2.應用程序調用read()

read(fd, &event, sizeof(struct input_event));

-----------------------------------------

vfs

sys_read(); // 系統調用

file->f_ops->read(); // fd就是file數組的下表,通過傳入的fd找到file,其中的f_ops在open()的時候已經獲取並保存

-----------------------------------------

static ssize_t evdev_read(struct file *file, char __user *buffer,
			  size_t count, loff_t *ppos)
{
	struct evdev_client *client = file->private_data; // 獲取open() 分配的緩衝區
	struct evdev *evdev = client->evdev;   // 獲取到evdev
	struct input_event event;	// 表示一個數據包,要給用戶
	size_t read = 0;
	int error;

	if (count != 0 && count < input_event_size())
		return -EINVAL;

	for (;;) {
		if (!evdev->exist || client->revoked)
			return -ENODEV;

		// 實現非阻塞 -- 隊列爲空,且爲非阻塞模式,直接返回again
		if (client->packet_head == client->tail &&   // 隊列的頭跟尾位置一樣 == 隊列爲空
		    (file->f_flags & O_NONBLOCK))   // 非阻塞
			return -EAGAIN;

		/*
		 * count == 0 is special - no IO is done but we check
		 * for error conditions (see above).
		 */
		if (count == 0)
			break;

		while (read + input_event_size() <= count &&    // 這裏判斷要取的數據個數,count是要取得數據個數
		       evdev_fetch_next_event(client, &event)) {  // 1. 從client的緩衝區取數據,放到event中

			if (input_event_to_user(buffer + read, &event))  // 2. 把數據給用戶空間
				return -EFAULT;

			read += input_event_size();    // 3. 統計上報多少數據
		}

		if (read)
			break;

		if (!(file->f_flags & O_NONBLOCK)) {   // 如果當前不是非阻塞模式,即阻塞模式
			error = wait_event_interruptible(evdev->wait,   // 休眠 - 條件不滿足就睡眠:
					client->packet_head != client->tail ||  // 隊列頭不等於尾 -> 有數據
					!evdev->exist || client->revoked);
			if (error)
				return error;
		}
	}

	return read;
}

總結:

1. 如果沒數據,就休眠等待

2. 如果有數據,就會從緩衝區client->buffer[client->tail++]拿數據,通過copy_to_user上報給用戶

疑問:

1. 數據到底是如何存放在緩衝區的

2. 等待隊列是誰喚醒的

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

上報流程:

input_event(buttons_dev, EV_KEY, pindesc->key_val, 0);

    |

    input_handle_event(dev, type, code, value);

        |

        if (disposition & INPUT_PASS_TO_HANDLERS) { // input device數據交給input handler處理

            struct input_value *v;

            v = &dev->vals[dev->num_vals++]; // 將input device獲取到的數據暫存到dev->vals

            v->type = type;

            v->code = code;

            v->value = value;

           input_pass_values(dev, dev->vals, dev->num_vals);

                |

                list_for_each_entry_rcu(handle, &dev->h_list, d_node) // 通過inpit device中與handle建立連接的 h_list 成員找到 handle

                if (handle->open)

                       input_to_handler(handle, vals, count);

                             |

                             struct input_handler *handler = handle->handler; // 通過出入的handle找到input handler(這裏是evdev)

                             if (handler->events) // 首選events(), 沒有才調用event()

                                     handler->events(handle, vals, count); // 調用events()

                             else if (handler->event)

                                     for (v = vals; v != end; v++)

                                             handler->event(handle, v->type, v->code, v->value);

static struct input_handler evdev_handler = {
    .event = evdev_event,
    ...
};
/*
 * Pass incoming events to all connected clients.
 */
static void evdev_events(struct input_handle *handle,
			 const struct input_value *vals, unsigned int count)
{
	struct evdev *evdev = handle->private; // 從handle中拿到evdev -- connect()中保存了:evdev->handle.private = evdev;
	struct evdev_client *client;
	/*	
		如果多個應用進程打開了同一個input device, 每次open()都會生成一個evdev_client
		evdev_client掛載到evdev的client_list鏈表中
		input_report_abs()時,handler會把數據copy到client_list所有的evdev_client的buffer中
		input_mt_sync(),逐一喚醒
		*/
	ktime_t ev_time[EV_CLK_MAX];

	ev_time[EV_CLK_MONO] = ktime_get();
	ev_time[EV_CLK_REAL] = ktime_mono_to_real(ev_time[EV_CLK_MONO]);
	ev_time[EV_CLK_BOOT] = ktime_mono_to_any(ev_time[EV_CLK_MONO],
						 TK_OFFS_BOOT);

	rcu_read_lock();

	client = rcu_dereference(evdev->grab);

        // 向每一個打開設備的程序發送數據包
	if (client)
		evdev_pass_values(client, vals, count, ev_time);  
	else
		list_for_each_entry_rcu(client, &evdev->client_list, node)
			evdev_pass_values(client, vals, count, ev_time);

	rcu_read_unlock();

}

evdev_pass_values(client, vals, count, ev_time);

static void evdev_pass_values(struct evdev_client *client,
			const struct input_value *vals, unsigned int count,
			ktime_t *ev_time)
{
	struct evdev *evdev = client->evdev;  // 通過client 獲取到 evdev
	const struct input_value *v;
	struct input_event event;    // 數據包
	bool wakeup = false;

	if (client->revoked)
		return;

	event.time = ktime_to_timeval(ev_time[client->clk_type]);  // 填充數據包中的時間戳

	/* Interrupts are disabled, just acquire the lock. */
	spin_lock(&client->buffer_lock);

	for (v = vals; v != vals + count; v++) {  // 將input device上報的數據封裝成 input_event對象
		if (__evdev_is_filtered(client, v->type, v->code))
			continue;

		// 喚醒等待隊列 -- 如果調用了input_sync() --  input_event(dev, EV_SYN, SYN_REPORT, 0);
		if (v->type == EV_SYN && v->code == SYN_REPORT) { 
			/* drop empty SYN_REPORT */
			if (client->packet_head == client->head)
				continue;

			wakeup = true;
		}

		event.type = v->type;
		event.code = v->code;
		event.value = v->value;
		__pass_event(client, &event);
	}

	spin_unlock(&client->buffer_lock);

	if (wakeup)
		wake_up_interruptible(&evdev->wait);  // 喚醒等待隊列
}
static void __pass_event(struct evdev_client *client,
			 const struct input_event *event)
{
	client->buffer[client->head++] = *event;  // 將input event數據放入緩衝區
	client->head &= client->bufsize - 1;

	if (unlikely(client->head == client->tail)) {
		/*
		 * This effectively "drops" all unconsumed events, leaving
		 * EV_SYN/SYN_DROPPED plus the newest event in the queue.
		 */
		client->tail = (client->head - 2) & (client->bufsize - 1);

		client->buffer[client->tail].time = event->time;
		client->buffer[client->tail].type = EV_SYN;
		client->buffer[client->tail].code = SYN_DROPPED;
		client->buffer[client->tail].value = 0;

		client->packet_head = client->tail;
	}

	if (event->type == EV_SYN && event->code == SYN_REPORT) { 
		client->packet_head = client->head;
		kill_fasync(&client->fasync, SIGIO, POLL_IN);
	}
}

  總結:
    1. 數據到底是如何存放在緩衝區的
        input_event()將數據交給handler,調用events(),將數據放入緩衝區client->buffer[client->head++] = *event;
    2. 等待隊列是誰喚醒的
        input_sync() 顯式喚醒等待隊列 wake_up_interruptible(&evdev->wait);

測試:
1.
hexdump /dev/event1  (open(/dev/event1), read(), )
           秒        微秒    類  code    value
0000000 0bb2 0000 0e48 000c 0001 0026 0001 0000
0000010 0bb2 0000 0e54 000c 0000 0000 0000 0000
0000020 0bb2 0000 5815 000e 0001 0026 0000 0000
0000030 0bb2 0000 581f 000e 0000 0000 0000 0000

2. 如果沒有啓動QT:
cat /dev/tty1
按:s2,s3,s4
就可以得到ls

或者:
exec 0</dev/tty1
然後可以使用按鍵來輸入


3. 如果已經啓動了QT:
可以點開記事本
然後按:s2,s3,s4

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章