xinput 模型简介
实现背景
考虑到当系统中存在多个 input 设备(多个 USB 设备或触摸屏同时存在)时,用户程序可以同时对其进行响应,并简化热插拔事件监测,SylixOS 实现了 xinput.ko 内核模块。
当注册该模块后,将创建两个设备,分别为/dev/input/xmse 和/dev/input/xkbd,分别收集系统中所有的鼠标和键盘事件,应用程序则只需要读取这两个设备即可。对于 xmse 设备,由于存在一般鼠标消息和触摸屏消息,因此,应用程序需要分别处理。
原理初探
热插拔检测
xinput 的热插拔检测使用了 SylixOS 的热插拔子系统,对应的 INPUT 设备驱动程序应向热插拔子系统的工作队列添加热插拔检测事件。
xinput 会检测 SylixOS 热插拔子系统 “/dev/hotplug” 的文件描述符,当描述符就绪时会判断是否是 INPUT 设备。若为 input 设备,则调用 open 函数打开该设备。
事件收集
用户调用 open 函数打开 xinput 创建的/dev/input/xmse 或/dev/input/xkbd 后,应调用 read 函数读取/dev/input/xmse 或/dev/input/xkbd 设备以获取 input 事件。xinput 中会将具体的每一个 input 设备产生的事件汇总到用户调用的该 read 接口,最终使用户获取 event 结构以获得 input 事件信息。
input 设备查看
xinput 提供了 proc 文件系统节点供用户查看系统当前的 input 设备情况。
代码分析
模块加载
加载 xinput.ko 模块时,默认会执行其 module_init 函数。
int module_init (void)
{
LW_CLASS_THREADATTR threadattr;
char temp_str[128];
int prio;
xdev_init(); /* xinput设备初始化 */
xinput_proc_init(); /* xinput proc文件系统初始化 */
if (xinput_drv()) { /* xinput驱动注册 */
return (PX_ERROR);
}
if (xinput_devadd()) { /* xinput设备创建 */
return (PX_ERROR);
}
LW_SPIN_INIT(&xinput_sl);
xinput_hotplug = TRUE;
if (getenv_r("XINPUT_KQSIZE", temp_str, sizeof(temp_str)) == 0) {
xinput_kmqsize = atoi(temp_str);
}
if (getenv_r("XINPUT_MQSIZE", temp_str, sizeof(temp_str)) == 0) {
xinput_mmqsize = atoi(temp_str);
}
threadattr = API_ThreadAttrGetDefault();
if (getenv_r("XINPUT_PRIO", temp_str, sizeof(temp_str)) == 0) {
prio = atoi(temp_str);
threadattr.THREADATTR_ucPriority = (uint8_t)prio;
}
threadattr.THREADATTR_ulOption |= LW_OPTION_OBJECT_GLOBAL;
threadattr.THREADATTR_stStackByteSize = XINPUT_THREAD_SIZE;
/*
* xinput扫描线程创建。
*/
xinput_thread = API_ThreadCreate("t_xinput",
xinput_scan,
&threadattr,
NULL);
return (ERROR_NONE);
}
xinput 模块加载时会通过环境变量 KEYBOARD 和 MOUSE 获取当前需要关注的 input 设备名称,并注册 xinput 设备驱动、建立 xinput 设备,最终创建 xinput 最重要的 xinput_scan 线程。
对于 QT 等 GUI 应用程序,可以在系统中设定 KEYBOARD 和 MOUSE 环境变量,用于告知 xinput 模块当前需要关注的 input 设备;同时也可以设定 XINPUT_KQSIZE 和 XINPUT_MQSIZE 环境变量,用于设置 xinput 设备支持的消息队列大小。相关的环境变量用例如下:
KEYBOARD=/dev/input/kbd0
MOUSE=/dev/input/touch0:/dev/input/mse0
XINPUT_KQSIZE=16
XINPUT_MQSIZE=4
xinput_scan 线程
xinput_scan 线程是 xinput.ko 模块最核心的逻辑实现,其代码如下:
static void *xinput_scan (void *arg)
{
INTREG int_lock;
int i;
fd_set fdset;
int width;
int ret;
struct timeval timeval = {XINPUT_SEL_TO, 0};
keyboard_event_notify knotify;
mouse_event_notify mnotify[MAX_INPUT_POINTS];
BOOL need_check;
(void)arg;
xinput_hotplug_fd = open("/dev/hotplug", O_RDONLY);
for (;;) {
FD_ZERO(&fdset);
LW_SPIN_LOCK_QUICK(&xinput_sl, &int_lock);
need_check = xinput_hotplug;
xinput_hotplug = FALSE;
LW_SPIN_UNLOCK_QUICK(&xinput_sl, int_lock);
if (need_check) {
xdev_try_open();
}
width = xdev_set_fdset(&fdset);
if (xinput_hotplug_fd >= 0) {
FD_SET(xinput_hotplug_fd, &fdset);
width = (width > xinput_hotplug_fd) ? width : xinput_hotplug_fd;
}
width += 1;
ret = select(width, &fdset, LW_NULL, LW_NULL, &timeval);
if (ret < 0) {
xdev_close_all();
LW_SPIN_LOCK_QUICK(&xinput_sl, &int_lock);
xinput_hotplug = TRUE;
LW_SPIN_UNLOCK_QUICK(&xinput_sl, int_lock);
sleep(XINPUT_SEL_TO);
continue;
} else if (ret == 0) {
continue;
} else {
ssize_t temp;
input_dev_t *input;
if (xinput_hotplug_fd >= 0 && FD_ISSET(xinput_hotplug_fd, &fdset)) {
unsigned char hpmsg[LW_HOTPLUG_DEV_MAX_MSGSIZE];
temp = read(xinput_hotplug_fd, hpmsg, LW_HOTPLUG_DEV_MAX_MSGSIZE);
if (temp > 0) {
xinput_hotplug_cb(hpmsg, (size_t)temp);
}
}
input = xdev_kbd_array;
for (i = 0; i < xdev_kbd_num; i++, input++) {
if (input->fd >= 0) {
if (FD_ISSET(input->fd, &fdset)) {
temp = read(input->fd, (PVOID)&knotify, sizeof(knotify));
if (temp <= 0) {
close(input->fd);
input->fd = -1;
} else {
if (LW_DEV_GET_USE_COUNT(&kdb_xinput.devhdr)) {
API_MsgQueueSend2(kdb_xinput.queue, (void *)&knotify,
(u_long)temp,
LW_OPTION_WAIT_INFINITE);
SEL_WAKE_UP_ALL(&kdb_xinput.sel_list, SELREAD);
}
}
}
}
}
input = xdev_mse_array;
for (i = 0; i < xdev_mse_num; i++, input++) {
if (input->fd >= 0) {
if (FD_ISSET(input->fd, &fdset)) {
temp = read(input->fd, (PVOID)mnotify,
sizeof(mouse_event_notify) * MAX_INPUT_POINTS);
if (temp <= 0) {
close(input->fd);
input->fd = -1;
} else {
if (LW_DEV_GET_USE_COUNT(&mse_xinput.devhdr)) {
if (!(mnotify[0].kstat & MOUSE_LEFT)) {
API_MsgQueueSend2(mse_xinput.queue, (void *)mnotify,
(u_long)temp,
LW_OPTION_WAIT_INFINITE);
} else {
API_MsgQueueSend(mse_xinput.queue, (void *)mnotify,
(u_long)temp);
}
SEL_WAKE_UP_ALL(&mse_xinput.sel_list, SELREAD);
}
}
}
}
}
}
}
return (NULL);
}
xinput_scan 线程创建之后会尝试打开 xinput 模块加载时已设置的需关注的 input 设备。若打开成功,则记录下其文件描述符 fd,作为当前需要关注的 input 设备之一。
xinput_scan 线程使用了多路 I/O 复用的 select 函数,将已打开的 input 设备文件描述符、系统热插拔设备“/dev/hotplug”文件描述符归入同一个描述符集,监听描述符集中所有描述符的状态。
因此,xinput 模块要求鼠标和键盘的读取 read 函数不能产生阻塞。如果没有事件产生,就立即返回读取失败,对鼠标和键盘的事件阻塞必须通过 select 完成,否则整个 xinput_scan 线程就会被某一个 input 设备的 read 函数阻塞。
当 select 返回时,首先会检查是否为 hotplug 热插拔设备产生的热插拔事件,调用 xinput_hotplug_cb 判断 msg_type 消息类型是否为 LW_HOTPLUG_MSG_USB_KEYBOARD 、 LW_HOTPLUG_MSG_USB_MOUSE 或 LW_HOTPLUG_MSG_PCI_INPUT ,如果是,则在线程下一次循环中再次去尝试打开刚插入的 input 设备。xinput_hotplug_cb 的实现如下:
static void xinput_hotplug_cb (unsigned char *phpmsg, size_t size)
{
INTREG int_lock;
int msg_type;
(void)size;
msg_type = (int)((phpmsg[0] << 24) +
(phpmsg[1] << 16) +
(phpmsg[2] << 8) +
phpmsg[3]);
if ((msg_type == LW_HOTPLUG_MSG_USB_KEYBOARD) ||
(msg_type == LW_HOTPLUG_MSG_USB_MOUSE) ||
(msg_type == LW_HOTPLUG_MSG_PCI_INPUT)) {
if (phpmsg[4]) {
LW_SPIN_LOCK_QUICK(&xinput_sl, &int_lock);
xinput_hotplug = TRUE;
LW_SPIN_UNLOCK_QUICK(&xinput_sl, int_lock);
}
}
}
检查完 hotplug 热插拔设备事件,xinput_scan 线程会循环读取每个键盘和每个鼠标、触摸屏设备的 input 消息。读取到的键盘 input 消息以 keyboard_event_notify 结构体形式提交,读取到的鼠标、触摸 input 消息以 mouse_event_notify 结构体形式提交。
当读取到 input 消息后,xinput 会调用系统的消息队列接口,将信息投递到 xinput 的 read 接口中,以供用户程序获取,并唤醒用户设置的待唤醒的线程。
设置待唤醒线程
xinput 的 ioctl 接口提供了可使用户程序线程休眠的命令,当 xinput 设备接收到 input 事件后可以将其唤醒。
应用程序需定义如下的 LW_SEL_WAKEUPNODE 结构体,将待休眠的线程 ID 赋值给 SELWUN_hThreadId,并调用 xinput 的 ioctl 接口,ioctl 的 cmd 为 FIOSELECT,arg 为如下的结构体变量地址,即可将线程加入到系统等待链表中。
#include <SylixOS.h>
typedef struct {
LW_LIST_LINE SELWUN_lineManage;
BOOL SELWUN_bDontFree;
LW_OBJECT_HANDLE SELWUN_hThreadId;
INT SELWUN_iFd;
LW_SEL_TYPE SELWUN_seltypType;
} LW_SEL_WAKEUPNODE;
typedef LW_SEL_WAKEUPNODE *PLW_SEL_WAKEUPNODE;
input 设备查看
xinput 同时实现了 proc 文件节点,读取/proc/xinput 文件即可查看到当前系统中的 input 设备,如下:
[root@sylixos:/]# cat /proc/xinput
devices type status
/dev/input/kbd0 kbd close
/dev/input/touch0 mse close
/dev/input/mse0 mse close