当前位置: 首页 > news >正文

input子系统的框架和重要数据结构详解

#1024程序员节 | 征文#
在这里插入图片描述
1

往期内容

I2C子系统专栏:

  1. 专栏地址:IIC子系统_憧憬一下的博客-CSDN博客
  2. 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客
    – 末篇,有往期内容观看顺序

总线和设备树专栏:

  1. 专栏地址:总线和设备树_憧憬一下的博客-CSDN博客
  2. 设备树与 Linux 内核设备驱动模型的整合-CSDN博客
    – 末篇,有往期内容观看顺序

1. 回顾字符设备驱动程序

img

1. 确定主设备号

字符设备驱动需要一个主设备号来标识。你可以选择一个静态的设备号,或者使用动态分配的设备号。

#define DEVICE_NAME "my_chrdev"
#define CLASS_NAME  "my_class"
static int major;  // 保存主设备号

可以通过 register_chrdev() 函数静态注册一个设备号,或者通过 alloc_chrdev_region() 动态分配设备号。

2. 创建 file_operations 结构体

file_operations 结构体定义了设备文件操作的函数指针,如打开、读写、控制等。你需要在这里填充相关的操作函数:

static int drv_open(struct inode *inode, struct file *file) {printk(KERN_INFO "Device opened\n");return 0;
}static int drv_release(struct inode *inode, struct file *file) {printk(KERN_INFO "Device closed\n");return 0;
}static ssize_t drv_read(struct file *file, char __user *buf, size_t len, loff_t *offset) {printk(KERN_INFO "Reading from device\n");return 0;  // 返回读取的字节数
}static ssize_t drv_write(struct file *file, const char __user *buf, size_t len, loff_t *offset) {printk(KERN_INFO "Writing to device\n");return len;  // 返回写入的字节数
}static struct file_operations fops = {.open = drv_open,.release = drv_release,.read = drv_read,.write = drv_write,
};

3. 注册 file_operations 结构体

通过 register_chrdev()file_operations 注册到内核中。如果主设备号是动态分配的,需要通过 major 保存分配的设备号。

static int __init my_driver_init(void) {// 动态分配主设备号major = register_chrdev(0, DEVICE_NAME, &fops);if (major < 0) {printk(KERN_ALERT "Failed to register char device\n");return major;}printk(KERN_INFO "Registered char device with major number %d\n", major);// 创建设备类my_class = class_create(THIS_MODULE, CLASS_NAME);if (IS_ERR(my_class)) {unregister_chrdev(major, DEVICE_NAME);printk(KERN_ALERT "Failed to create class\n");return PTR_ERR(my_class);}// 创建设备节点my_device = device_create(my_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME);if (IS_ERR(my_device)) {class_destroy(my_class);unregister_chrdev(major, DEVICE_NAME);printk(KERN_ALERT "Failed to create device\n");return PTR_ERR(my_device);}return 0;
}

在这里,我们使用 class_create()device_create() 来创建设备类和设备节点,这样用户空间可以通过 /dev/my_chrdev 访问设备。

4. 编写入口函数

入口函数是模块加载时执行的代码,一般用于初始化设备、分配资源等。

static int __init my_driver_init(void) {printk(KERN_INFO "Initializing character device driver\n");// 注册字符设备major = register_chrdev(0, DEVICE_NAME, &fops);if (major < 0) {printk(KERN_ALERT "Failed to register a major number\n");return major;}printk(KERN_INFO "Registered with major number %d\n", major);// 创建类和设备节点my_class = class_create(THIS_MODULE, CLASS_NAME);if (IS_ERR(my_class)) {unregister_chrdev(major, DEVICE_NAME);printk(KERN_ALERT "Failed to register device class\n");return PTR_ERR(my_class);}my_device = device_create(my_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME);if (IS_ERR(my_device)) {class_destroy(my_class);unregister_chrdev(major, DEVICE_NAME);printk(KERN_ALERT "Failed to create the device\n");return PTR_ERR(my_device);}printk(KERN_INFO "Device class created\n");return 0;
}

5. 编写出口函数

当卸载驱动时需要释放相关资源,这个过程在出口函数中完成。

static void __exit my_driver_exit(void) {device_destroy(my_class, MKDEV(major, 0));  // 删除设备节点class_unregister(my_class);  // 注销设备类class_destroy(my_class);  // 销毁类unregister_chrdev(major, DEVICE_NAME);  // 注销字符设备printk(KERN_INFO "Character device driver unloaded\n");
}

6. 辅助函数

使用 class_createdevice_create 可以帮助自动创建 /dev 中的设备节点,从而方便用户访问。

  • class_create(THIS_MODULE, CLASS_NAME):创建一个类
  • device_create(my_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME):在 /dev 中创建设备节点

2. Input子系统框架

img

从图中可以看到,整个 Linux 输入子系统架构可以分为三大层:Input Device 层Input Core 层Input Handler 层

1. Input Device 层(输入设备层)

这个层主要是和硬件直接打交道的地方,如键盘、鼠标、触摸屏、GPIO
键等输入设备。输入设备的驱动程序从硬件中获取数据,并将这些原始数据转换为标准的输入事件,然后上报给核心层(Input Core)。

核心组件:

  • input_dev 结构体:这是设备驱动程序和输入子系统交互的核心结构体。它定义了设备驱动程序如何报告事件,比如按键按下、鼠标移动等。

简要流程: 硬件设备产生输入信号(如按下按键),设备驱动程序通过 input_dev 将这些输入信号传递给 Input Core。

2. Input Core 层(输入核心层)

这个层负责接收来自底层输入设备的事件,并将这些事件分发给上层的处理器(Handler)。它起到桥梁的作用,将底层设备的事件转换为上层可以理解的标准化事件。

核心组件:

  • input.c:是 Input Core 层的主要模块,它提供了 API 和机制来管理输入设备和输入事件。输入核心会将设备上报的事件(如键值、坐标)转交给处理这些事件的 handler

功能概述

  • 接收底层设备上报的输入事件。
  • 将事件转发给合适的 handler 进行处理。

3. Input Handler 层(输入处理层)

这个层负责处理输入核心层转发的输入事件,并向用户空间提供访问接口。在 Linux 中,evdev
是最常用的处理层,负责将输入事件暴露给用户空间。

核心组件:

  • evdev.c:该模块将输入设备的事件通过 /dev/input/eventX 设备节点暴露给用户空间的应用程序,应用程序可以通过 open()read()ioctl() 等系统调用来访问这些事件。

file_operations 结构体evdev_fops 定义了如何处理用户空间对输入设备的访问。这个结构体包括了多个操作函数,比如:

  • .read:读取输入事件。
  • .write:写入数据。
  • .poll:轮询事件。
  • .ioctl:处理设备的控制命令。

4. 用户空间访问

用户空间的应用程序可以通过 /dev/input/eventX 设备节点直接访问输入设备。这些应用程序通过
open()ioctl()poll()read() 等接口访问设备数据,处理输入事件。

举例:

  • tslib:是用于处理触摸屏输入的库。
  • libinput:是一个更高级的输入处理库,用于统一处理键盘、鼠标、触摸屏等设备的输入事件。

Input Device 层:负责将硬件设备的原始输入信号转换为标准事件。

Input Core 层:负责管理输入设备,并将标准事件传递给处理层。

Input Handler 层:将事件暴露给用户空间,用户通过访问 /dev/input/eventX 来读取输入事件。

3. Input子系统重要结构体

内核源码:

  • \Linux-4.9.88\include\linux\input.h📎input.h
  • \Linux-4.9.88\include\uapi\linux\input-event-codes.h📎input-event-codes.h

img

3.1 Input_dev

truct input_dev 是描述输入设备的核心结构体。它提供了输入设备的详细属性、支持的事件类型、处理事件的回调函数等。在开发输入设备驱动时,开发者需要初始化并注册该结构体,使其能够正确地处理输入事件并将其传递到上层。

\Linux-4.9.88\include\linux\input.h
struct input_dev {const char *name; //输入设备的名称。例如,"USB Keyboard"const char *phys; //输入设备的物理路径。例如,"usb-0000:00:1d.0-1/input0"。const char *uniq; //输入设备的唯一标识符,通常用于区分具有相同类型的多个设备。struct input_id id; //输入设备的硬件 ID,用于标识输入设备的制造商、产品 ID 和版本号。unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)];unsigned long evbit[BITS_TO_LONGS(EV_CNT)];//设备支持的事件类型。事件类型定义在 linux/input.h 中,如 EV_KEY 表示按键事件,EV_ABS 表示绝对坐标事件。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)];//设备支持的 LED 事件(如键盘指示灯)。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 hint_events_per_packet;// 每个数据包的建议事件数,优化设备的事件处理。unsigned int keycodemax;unsigned int keycodesize;//输入设备支持的最大键码数和每个键码的大小。void *keycode;//存储设备支持的键码映射表。int (*setkeycode)(struct input_dev *dev,const struct input_keymap_entry *ke,unsigned int *old_keycode);//设置设备的键码映射。int (*getkeycode)(struct input_dev *dev,struct input_keymap_entry *ke);// 获取设备的键码映射。struct ff_device *ff;//力反馈设备的相关信息。unsigned int repeat_key;//保存最后一个重复的按键值。struct timer_list timer;//用于处理按键重复事件的定时器int rep[REP_CNT];//按键重复相关的参数,主要用于延迟与频率。struct input_mt *mt;//多点触控相关的数据结构。struct input_absinfo *absinfo;//保存绝对坐标相关的信息(如最小值、最大值、分辨率等)。unsigned long key[BITS_TO_LONGS(KEY_CNT)];//保存按键的当前状态unsigned long led[BITS_TO_LONGS(LED_CNT)];//保存 LED 的当前状态unsigned long snd[BITS_TO_LONGS(SND_CNT)];//保存声音设备的当前状态unsigned long sw[BITS_TO_LONGS(SW_CNT)];//保存开关的当前状态。int (*open)(struct input_dev *dev);//打开设备时调用的函数,通常用于初始化设备。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 __rcu *grab;spinlock_t event_lock;//用于保护输入事件处理的自旋锁。struct mutex mutex;//用于保护设备其他部分的互斥锁。unsigned int users;//记录打开该设备的用户数。bool going_away;//标识设备是否正在被卸载。struct device dev;//内核设备模型中的设备结构体。struct list_head	h_list; // 保存handle链表struct list_head	node;//设备在输入子系统链表中的挂载点。unsigned int num_vals;unsigned int max_vals;//当前设备的输入值数量及其最大值。struct input_value *vals;//保存输入值的数组bool devres_managed;//标识设备是否由资源管理器管理
};

3.2 input_handler

input_handler 作为输入事件的处理器,它的作用在于:

  • 事件的接收与处理:它定义了如何接收和处理来自输入设备的事件,如键盘按键、鼠标移动等。
  • 事件过滤filter 函数可以在事件传递前过滤掉不需要的事件,提高处理效率。
  • 设备匹配与连接:通过 matchconnect 函数,它负责将处理程序与适当的设备连接起来。
  • 事件传递eventevents 回调函数可以在设备产生事件时,将事件传递给上层逻辑,或通过驱动程序接口传递给用户空间应用。

工作流程简述

  1. 设备连接:当一个输入设备注册时,input_handlermatch 函数会被调用,以确定该处理程序是否可以处理这个设备。如果匹配成功,connect 函数被调用来建立连接。
  2. 事件处理:一旦设备产生事件,eventevents 回调会被调用来处理输入事件。根据设备和处理程序的实现,事件可以传递给用户空间或在内核中进一步处理。
  3. 设备断开:当设备断开时,disconnect 函数会被调用,处理程序负责清理与设备连接相关的资源。
\Linux-4.9.88\include\linux\input.h
struct input_handler {void *private;//用于存储处理程序的私有数据。驱动程序可以使用它来保存与特定设备相关的上下文信息。void (*event)(struct input_handle *handle, unsigned int type, unsigned int code, int value);//这是处理单个输入事件的回调函数。事件由 type、code 和 value 表示,分别代表事件类型、事件码和事件的值(如按键按下或松开、移动坐标等)。void (*events)(struct input_handle *handle,const struct input_value *vals, unsigned int count);//处理多个输入事件的回调函数。vals 是一个包含多个事件的数组,count 是事件的数量。这允许处理一组输入事件,提高效率。bool (*filter)(struct input_handle *handle, unsigned int type, unsigned int code, int value);//这是一个事件过滤函数。它用于判断某个事件是否应该被传递给上层。如果返回 true,则事件会被过滤掉,不会继续处理;如果返回 false,事件将继续传递。bool (*match)(struct input_handler *handler, struct input_dev *dev);//用于匹配处理程序和输入设备。返回 true 表示当前 handler 可以处理 dev 设备。int (*connect)(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id);//当有输入设备与 handler 匹配时调用的回调函数,用于连接设备。返回 0 表示成功连接,其他值表示失败。void (*disconnect)(struct input_handle *handle);//当输入设备断开时调用的回调函数,用于处理设备断开连接后的清理工作。void (*start)(struct input_handle *handle);//在 connect 成功之后调用的回调函数,通常用于初始化某些特定资源或启动设备处理程序。bool legacy_minors;//指示处理程序是否使用了旧的次设备号 (minor number) 机制。这个标志主要用于向后兼容旧的输入设备模型。int minor;//用于存储处理程序关联的次设备号。次设备号帮助区分同一类设备中的不同实例。const char *name;//输入处理程序的名称,用于在内核日志或其他信息输出中标识处理程序。const struct input_device_id *id_table;//这是一个输入设备 ID 表,用于定义处理程序可以匹配的输入设备类型。它通常包含输入设备的厂商 ID、产品 ID 等信息,用于识别设备。struct list_head	h_list;//用于将 input_handler 添加到输入子系统的处理程序列表handle中,和相应的input_dev匹配保存进去struct list_head	node;//用于将 input_handler 添加到设备的处理程序链表中。
};

3.3 input_handle

输入子系统中的一个核心结构体,它用于连接输入设备 (input_dev) 和输入事件处理程序 (input_handler)。当设备和处理程序之间建立连接时,input_handle 负责维护两者之间的关联,确保事件能够从设备传递到合适的处理程序进行处理。

工作流程简述

  1. 设备与处理程序连接:当输入设备与输入处理程序匹配成功时,系统会创建并初始化一个 input_handle,用来维持设备和处理程序的关系。
  2. 事件传递:一旦输入设备产生事件,input_handle 会将这些事件从 input_dev 传递给对应的 input_handler 进行处理。
  3. 设备打开/关闭管理input_handle 中的 open 标志位用于记录设备的状态,确保设备只在被打开时接收和处理事件,并且在关闭时停止事件处理。
  4. 设备断开:当输入设备断开连接时,系统会调用 input_handlerdisconnect 函数,并清理 input_handle 中的资源。
\Linux-4.9.88\include\linux\input.h
struct input_handle {void *private;//这个指针用于保存输入处理程序的私有数据(上下文数据)。//输入子系统在连接设备时,可能需要为每个连接的 input_handle 维护独立的数据。int open; // 这个整数值用于表示设备是否已经打开。const char *name;//处理程序的名称,通常用于标识该输入处理程序,可以帮助调试和日志输出时识别当前使用的 input_handle。struct input_dev *dev;//它表示 input_handle 正在处理的输入设备。struct input_handler *handler;//它表示当前 input_handle 所使用的处理程序。struct list_head	d_node;//这个链表节点用于将 input_handle 挂接到输入设备的处理程序链表中。这意味着一个输入设备可以有多个 input_handle 进行事件处理。struct list_head	h_node; //同里
};

3.4 input_event/value

是 Linux 输入子系统中用来描述输入设备事件的核心结构体,它表示从输入设备(如键盘、鼠标、触摸屏等)发送到内核的每个输入事件。

\Linux-4.9.88\include\linux\input.h
struct input_event {struct timeval time; //用来记录事件发生的时间。它包括秒 (tv_sec) 和微秒 (tv_usec) 两个字段,表示该事件的时间戳。__u16 type; // 表示事件的类型。它决定了事件的类别,比如按键事件、相对移动事件、绝对位置事件等。__u16 code;//表示具体的事件代码。其含义依赖于 type 的值。__s32 value;//表示事件的值,其含义取决于 type 和 code 的组合。
};struct input_value { //差不多,用这个比较多好像__u16 type;__u16 code;__s32 value;
};

type:

\Linux-4.9.88\include\uapi\linux\input-event-codes.h
/** Event types*/
#define EV_SYN			0x00
#define EV_KEY			0x01
#define EV_REL			0x02
#define EV_ABS			0x03
#define EV_MSC			0x04
#define EV_SW			0x05
#define EV_LED			0x11
#define EV_SND			0x12
#define EV_REP			0x14
#define EV_FF			0x15
#define EV_PWR			0x16
#define EV_FF_STATUS		0x17
#define EV_MAX			0x1f
#define EV_CNT			(EV_MAX+1)

这些宏定义描述了 Linux 输入子系统中不同的 事件类型 (Event Types)。这些类型是用于标识从输入设备(例如键盘、鼠标、触摸屏等)传递到内核的事件类别,每个类别都有不同的意义。struct input_event 结构体中的 type 字段使用这些宏来确定事件的类别。

  1. EV_SYN (0x00)
  • 同步事件,用于标识一组输入事件的结束。例如,多个输入事件会通过 EV_SYN 通知系统完成一个事件序列。这是一个标志位,告诉系统已经处理完一个事件的所有数据。
  • 典型用途:报告鼠标或触摸屏输入后,系统应通过 EV_SYN 告诉用户空间事件已完成。
  1. EV_KEY (0x01)
  • 按键事件,表示设备上的按键或按钮状态的变化。这是键盘、鼠标按钮、游戏控制器按钮等输入设备常用的事件类型。
  • 典型用途:键盘按下某个按键(如 KEY_A)或者鼠标点击时触发。
  1. EV_REL (0x02)
  • 相对坐标事件,用于描述相对于当前坐标的位移量。常用于鼠标等设备,表示相对位置的改变。
  • 典型用途:鼠标移动过程中生成的相对位移数据(REL_X, REL_Y)。
  1. EV_ABS (0x03)
  • 绝对坐标事件,用于描述设备的绝对坐标值。常用于触摸屏、操纵杆等设备,表示其当前位置。
  • 典型用途:触摸屏上的触摸点的 X 和 Y 位置(ABS_X, ABS_Y)。
  1. EV_MSC (0x04)
  • 杂项事件,用于发送某些特定的输入设备数据,例如时间戳或者扫描码等。
  • 典型用途:键盘扫描码事件(MSC_SCAN)。
  1. EV_SW (0x05)
  • 开关事件,用于表示设备上的开关状态。例如,表示设备是否插入、盖子是否关闭等。
  • 典型用途:检测设备状态(如笔记本的盖子是否关闭)。
  1. EV_LED (0x11)
  • LED 事件,用于控制设备上的 LED 指示灯。例如,键盘上的大写锁定指示灯(Caps Lock)、数字锁定指示灯(Num Lock)等。
  • 典型用途:设置或读取键盘上的 LED 状态。
  1. EV_SND (0x12)
  • 声音事件,用于产生音频信号,例如蜂鸣器。
  • 典型用途:系统蜂鸣器发出声音。
  1. EV_REP (0x14)
  • 重复事件,用于控制按键重复行为。例如,按住某个键时,定义按键重复的时间间隔(重复速度和延迟)。
  • 典型用途:按住某个键时产生的重复输入。
  1. EV_FF (0x15)
  • 力反馈事件,用于与具有力反馈功能的设备(如游戏控制器)进行交互,产生物理反馈(震动等)。
  • 典型用途:控制游戏控制器的震动反馈。
  1. EV_PWR (0x16)
  • 电源事件,用于报告或控制设备的电源状态。
  • 典型用途:电源按钮或设备的电源管理事件。
  1. EV_FF_STATUS (0x17)
  • 力反馈状态事件,用于报告力反馈设备的状态。
  • 典型用途:检测力反馈是否成功启动或停止。
  1. EV_MAX (0x1f)
  • 事件类型的最大值,用于定义事件类型的范围。
  1. EV_CNT (EV_MAX + 1)
  • 事件类型的数量(总计 32 种事件类型)。用于计算事件类型的总数。

这些事件类型用于在内核和用户空间之间传递输入设备的事件信息,驱动程序根据事件类型来处理输入设备的数据。例如:

  • 键盘驱动程序会处理 EV_KEY 类型的事件,用于表示按键的按下与松开。
  • 鼠标驱动程序会处理 EV_REL 类型的事件,用于表示鼠标的移动。
  • 触摸屏驱动程序会处理 EV_ABS 类型的事件,用于表示触摸点的坐标。

通过这些事件类型,输入子系统可以统一处理不同输入设备的数据,并通过 /dev/input/eventX 接口传递给用户空间程序。

code:

以type为EV_KEY按键类型例子:

#define KEY_RESERVED		0
#define KEY_ESC			1
#define KEY_1			2
#define KEY_2			3
#define KEY_3			4
#define KEY_4			5
#define KEY_5			6
#define KEY_6			7
#define KEY_7			8
#define KEY_8			9
#define KEY_9			10
.......

以type为EV_ABS绝对位移类型例子:

/** Absolute axes*/
#define ABS_X			0x00 //x方向
#define ABS_Y			0x01 /y方向
#define ABS_Z			0x02
#define ABS_RX			0x03
.......

value:

对于按键,它的 value 可以是 0(表示按键被按下)、1(表示按键被松开)、2(表示长按);

对于触摸屏,它的 value 就是坐标值、压力值。


http://www.mrgr.cn/news/57345.html

相关文章:

  • 记录:网鼎杯2024赛前热身WEB01
  • 微信小程序-获取头像和昵称
  • deepinlinux v23安装pl2303的usb串口驱动
  • 与ai一起作诗(《校园清廉韵》)
  • 2024软件测试面试秘籍(含答案+文档)
  • 打开游戏提示丢失(或找不到)XINPUT1_3.DLL的多种解决办法
  • SpringBoot项目整合Mybatis-MySql数据库编程
  • 总集篇:环形链表(是否成环?环长度?入环点?)
  • 鸿蒙启航 | 搭建 HarmonyOS 开发环境来个 Hello World
  • Jenkins配置CI/CD开发环境(理论到实践的完整流程)
  • opencv 将相机图片转为视频 - python 实现
  • 计算机毕业设计Hadoop+大模型在线教育大数据分析可视化 学情分析 课程推荐系统 机器学习 深度学习 人工智能 大数据毕业设计
  • 信发软件之添加组件——未来之窗行业应用跨平台架构
  • 顺序表(一)(数据结构)
  • linux:线程id及线程互斥
  • python基础综合案例(数据可视化—折线图可视化)
  • 全栈面试题】模块5-1】Oracle/MySQL 数据库基础
  • Spring Cloud --- Sentinel 规则持久化
  • 前端-基础CSS总结常用
  • 七、数据库服务器(MySQL、PostgreSQL)的搭建
  • 基于Fourier的两个人形机器人:从改进的3D扩散策略之iDP3到从单个RGB视频中模仿学习的OKAMI
  • 【面试经典150】day 6
  • Flutter鸿蒙next 中如何实现 WebView【跳、显、适、反】等一些基础问题
  • 项目太多,拓展固态硬盘,要安装软件如何固定移动硬盘盘符? - 解决必剪本地作品丢失的问题
  • 如何在复杂的信息物理系统中实施风险管理
  • Educational Codeforces Round 170 C New Game