xinput 模型简介

更新时间:
2024-01-09
下载文档

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 模块加载时会通过环境变量 KEYBOARDMOUSE 获取当前需要关注的 input 设备名称,并注册 xinput 设备驱动、建立 xinput 设备,最终创建 xinput 最重要的 xinput_scan 线程。

对于 QT 等 GUI 应用程序,可以在系统中设定 KEYBOARDMOUSE 环境变量,用于告知 xinput 模块当前需要关注的 input 设备;同时也可以设定 XINPUT_KQSIZEXINPUT_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_KEYBOARDLW_HOTPLUG_MSG_USB_MOUSELW_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
文档内容是否对您有所帮助?
有帮助
没帮助