嵌入式Linux输入系统应用编程学习总结

嵌入式Linux输入系统应用编程学习总结

目录

  • 嵌入式Linux输入系统应用编程学习总结
    • 1. 嵌入式Linux输入系统介绍
    • 2. Linux设备输入数据的几个结构体
      • 2.1 内核中表示一个输入设备的结构体
      • 2.2 APP从输入设备获取的数据类型结构体
    • 3. 查看LCD设备信息和输入数据
      • 3.1 查看设备信息
      • 3.2 使用命令读取输入的数据
      • 3.3 4种机制获取LCD设备输入数据
        • 3.3.1 使用ioctl获取设备信息
        • 3.3.2 查询方式(Polling)和休眠唤醒方式(Blocking I/O)获取设备输入的信息
        • 3.3.3 poll方式(Polling with poll)获取设备输入的信息
        • 3.3.4 selsct方式(Polling with select)获取设备输入的信息
        • 3.3.5 异步通知方式(Asynchronous I/O)获取设备输入的信息

基于韦东山IMX6ULL开发板和配套资料学习

参考教程:韦东山老师教程

1. 嵌入式Linux输入系统介绍

嵌入式Linux输入系统是处理来自各种输入设备(如键盘、鼠标、触摸屏等)事件的核心组件。在嵌入式系统中,输入子系统的设计需要特别考虑资源限制、实时性要求等因素。

从应用层到硬件最底层分为:用户空间(应用层)、内核空间(输入系统事件层、输入系统核心层、输入系统驱动层)、硬件(如键盘、鼠标、触摸屏等):

在这里插入图片描述

假设用户程序直接访问/dev/input/event0 设备节点,或者使用 tslib 访问设备节点的流程如下:

  • APP 发起读操作,若无数据则休眠
  • 用户操作设备,硬件上产生中断
  • 输入系统驱动层对应的驱动程序处理中断:读取到数据,转换为标准的输入事件,向核心层汇报,输入事件就是一个“ struct input_event”结构体
  • 核心层可以决定把输入事件转发给上面哪个 handler 来处理
    • 从 handler 的名字来看,它就是用来处输入操作的。有多种 handler,比如: evdev_handler、 kbd_handler、 joydev_handler 等等
    • 最常用的是 evdev_handler:它只是把 input_event 结构体保存在内核 buffer 等, APP 来读取时就原原本本地返回。它支持多个 APP 同时访问输入设备,每个 APP 都可以获得同一份输入事件
    • 当 APP 正在等待数据时, evdev_handler 会把它唤醒,这样 APP 就可以返回数据
  • APP 对输入事件的处理
    • APP 获得数据的方法有 2 种:
      • 直接访问设备节点(比如/dev/input/event0,1,2,…)
      • 通过 tslib、 libinput 这类库来间接访问设备节点,这些库简化了对数据的处理

2. Linux设备输入数据的几个结构体

基于编写应用程序的角度,只需要理解这些内容:

2.1 内核中表示一个输入设备的结构体

使用 input_dev 结构体来表示输入设备:

/* include/linux/input.h */
struct input_dev {const char *name;                  /* 设备名称 */const char *phys;                  /* 设备物理路径 */const char *uniq;                  /* 设备唯一标识符 */struct input_id id;                /* 输入设备ID */unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)]; /* 设备属性位图 */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)];         /* 支持的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;                              /* 句柄列表 */struct list_head node;                                /* 设备节点 */unsigned int num_vals;                                /* 当前值的数量 */unsigned int max_vals;                                /* 最大值的数量 */struct input_value *vals;                             /* 当前值数组 */bool devres_managed;                                  /* 设备资源管理标志 */
};

2.2 APP从输入设备获取的数据类型结构体

可以得到一系列的输入事件,就是一个一个“ struct input_event”:

/* include/uapi/linux/input.h */
struct input_event {struct timeval time;  /* 事件发生的时间戳 */__u16 type;           /* 事件类型 */__u16 code;           /* 事件代码 */__s32 value;          /* 事件值 */
};/* include/uapi/linux/time.h */
struct timeval {__kernel_time_t tv_sec;  /* 秒 */__kernel_suseconds_t tv_usec;  /* 微秒 */
};

其中事件类型type:

/* 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            //LED事件。表示LED状态的变化。用于表示LED的状态变化,如键盘上的Caps Lock灯。
#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)      //事件类型的计数。用于计算事件类型的总数,常用于数组或位图的大小计算。

其中事件代码code:

在这里插入图片描述

  • 键盘事件(EV_KEY)

    • KEY_:表示键盘上的具体按键,如KEY_A表示“A”键,KEY_NUMLOCK表示数字锁定键等。这些按键代码通常定义在/usr/include/linux/input-event-codes.h文件中。

    • BTN_:虽然主要用于按钮类设备,但某些键盘上的特殊按键或功能也可能使用BTN_前缀的代码,如BTN_LEFT(通常用于鼠标,但在某些上下文中可能表示键盘上的特殊功能键)。

  • 鼠标事件

    • REL_:表示相对坐标事件,如REL_XREL_Y分别表示鼠标在X轴和Y轴上的相对移动量,REL_WHEEL表示鼠标滚轮的滚动方向和量。

    • BTN_:表示鼠标按钮的按下和抬起事件,如BTN_LEFT(左键)、BTN_RIGHT(右键)、BTN_MIDDLE(中键/滚轮键)等。

  • 触摸屏事件

    • ABS_:表示绝对坐标事件,如ABS_MT_POSITION_XABS_MT_POSITION_Y分别表示触摸屏上触摸点的X轴和Y轴坐标。多点触控协议中可能还包含其他ABS_类型的代码,如ABS_MT_SLOT(表示多点触控中的触点编号)、ABS_MT_TRACKING_ID(用于跟踪触点的唯一标识符)等。

    • BTN_TOUCH:表示触摸屏上的触摸事件,当触摸发生时,该代码的值会被设置为1。

  • 其他事件类型

    • EV_MSCEV_SWEV_LEDEV_SNDEV_REPEV_FFEV_PWR等事件类型也有各自的code代码,但这些类型通常用于更专业的输入设备或特定功能,如杂项输入(MSC)、开关输入(SW)、LED控制(LED)、声音输出(SND)、自动重复(REP)、力反馈(FF)和电源管理(PWR)等。

其中事件值value:

在这里插入图片描述

  • 按键事件(EV_KEY)

    • 0:表示按键被释放或未按下。

    • 1:表示按键被按下。

    • 2(或其他非0非1值,取决于具体实现):在某些情况下,可能表示按键的长按或其他特殊状态,但这并不是标准的用法,更多依赖于具体设备和驱动程序的实现。

  • 相对坐标事件(EV_REL)

    • 对于REL_XREL_Y等相对坐标事件,value表示在X轴或Y轴上的相对移动量。正值通常表示向右或向下的移动,负值表示向左或向上的移动。

    • 对于REL_WHEEL等滚轮事件,value的正负表示滚轮滚动的方向,而绝对值通常表示滚动的量或步数。例如,value为1可能表示滚轮向上滚动一步,而-1则表示向下滚动一步。

  • 绝对坐标事件(EV_ABS)

    • 对于触摸屏和绝对定位设备,value表示触摸点或设备在X轴、Y轴上的绝对位置,或者表示其他绝对量(如压力、大小等)。

    • 具体的含义取决于code字段的值。例如,ABS_XABS_Y分别表示触摸点在X轴和Y轴上的位置,而ABS_PRESSURE则表示触摸点的压力值。

  • 其他事件类型

    • 对于其他事件类型(如EV_MSC、EV_SW、EV_LED等),value的含义可能因事件类型而异。这些类型的事件通常用于更专业的输入设备或特定功能。

    • 例如,在EV_MSC(杂项事件)中,value可能用于表示设备的特定状态或配置。在EV_SW(开关事件)中,value可能表示开关的打开或关闭状态。

事件之间的界线:

APP 读取数据时,可以得到一个或多个数据,比如一个触摸屏的一个触点会上报 X、 Y 位置信息,也可能会上报压力值。

APP 怎么知道它已经读到了完整的数据?

驱动程序上报完一系列的数据后,会上报一个“同步事件”,表示数据上报完毕。 APP 读到“同步事件”时,就知道已经读完了当前的数据。同步事件也是一个 input_event 结构体,它的 type、 code、 value 三项都是 0。

输入子系统支持:阻塞、非阻塞、 POLL/SELECT、异步通知的机制。

3. 查看LCD设备信息和输入数据

使用韦东山老师IMX6ULL Pro开发板和配套的LCD屏进行实操。

3.1 查看设备信息

输入设备的设备节点名为/dev/input/eventX(也可能是/dev/eventX, X 表示 0、 1、 2 等数字)。查看设备节点,可以执行以下命令:

ls /dev/input/* -l

ls /dev/event* -l

可以看到下图类似下面的信息:

在这里插入图片描述

可以在板子上执行以下命令,查看这些设备节点对应什么硬件:

cat /proc/bus/input/devices

这条指令的含义就是获取与 event 对应的相关设备信息,可以看到类似以下的结果:

在这里插入图片描述

  • I:id of the device(设备 ID):该参数由结构体 struct input_id 来进行描述,驱动程序中会定义这样的结构体:

    在这里插入图片描述

  • N:name of the device:设备名称

  • P:physical path to the device in the system hierarchy:系统层次结构中设备的物理路径

  • S:sysfs path:位于 sys 文件系统的路径

  • U:unique identification code for the device(if device has it):设备的唯一标识码

  • H:list of input handles associated with the device:与设备关联的输入句柄列表

  • B:bitmaps(位图)

    PROP:device properties and quirks(设备属性)
    EV:types of events supported by the device(设备支持的事件类型)
    KEY:keys/buttons this device has(此设备具有的键/按钮)
    MSC:miscellaneous events supported by the device(设备支持的其他事件)
    LED:leds present on the device(设备上的指示灯)
    

    需要注意的是 B 位图,比如上图中“ B: EV=b”用来表示该设备支持哪类输入事件。 b 的二进制是 1011, bit0、 1、 3 为 1,表示该设备支持 0、 1、 3 这三类事件,即 EV_SYN、 EV_KEY、 EV_ABS。

举一个例子,“ B: ABS=2658000 3”如何理解?

它表示该设备支持 EV_ABS 这一类事件中的哪一些事件。这是 2 个 32 位的数 字: 0x2658000、 0x3, 高位在 前低 位在 后, 组成一 个 64 位 的数字 :“ 0x2658000,00000003”,数值为 1 的位有: 0、 1、 47、 48、 50、 53、 54,即:0、 1、 0x2f、 0x30、 0x32、 0x35、 0x36,对应以下这些宏:

在这里插入图片描述

即这款输入设备支持上述的 ABS_X 、 ABS_Y 、 ABS_MT_SLOT 、 ABS_MT_TOUCH_MAJOR 、 ABS_MT_WIDTH_MAJOR 、 ABS_MT_POSITION_X 、ABS_MT_POSITION_Y 这些绝对位置事件。

3.2 使用命令读取输入的数据

调试输入系统时,直接执行类似下面的命令,然后操作对应的输入设备即可读出数据:

hexdump /dev/input/event0

在开发板上执行上述命令之后,点击按键或是点击触摸屏,就会打印下图信息:

在这里插入图片描述

上图中的 type 为 3 , 对应 EV_ABS ; code 为 0x35 对应 ABS_MT_POSITION_X; code 为 0x36 对应 ABS_MT_POSITION_Y。

上图中还发现有 2 个同步事件:它的 type、 code、 value 都为 0。表示电容屏上报了 2 次完整的数据。

3.3 4种机制获取LCD设备输入数据

读取输入数据的4种方式是:查询方式、休眠唤醒方式、poll方式、异步通知方式。

编程获取LCD设备输入数据之前需要先了解如何获取设备信息。

3.3.1 使用ioctl获取设备信息

ioctl(input/output control)是Linux中一个非常强大且灵活的函数,用于对设备文件的特定操作进行控制。这个函数提供了一种机制,使得用户空间程序能够向设备驱动程序发送控制命令或查询设备状态,而这些操作通常无法通过普通的read/write调用完成。

#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);

输入参数:

  • fd:文件描述符,指向要控制的设备文件。
  • request:设备特定的请求码,用于告诉驱动程序要执行哪种操作。
  • ...:一个可变参数列表,具体取决于request,可以是整数、指针等。

返回值:

  • 成功时,ioctl返回0;
  • 失败时,返回-1,并设置errno以指示错误类型。

有些驱动程序对 request 的格式有要求,它的格式如下:

在这里插入图片描述

比如 dir 为_IOC_READ(即 2)时,表示 APP 要读数据;为_IOC_WRITE(即 4)时,表示 APP 要写数据。

  • size 表示这个 ioctl 能传输数据的最大字节数。
  • type、 nr 的含义由具体的驱动程序决定。

比如要读取输入设备的 evbit 时, ioctl 的 request 要写为“ EVIOCGBIT(0, size)”, size 的大小可以由你决定:你想读多少字节就设置为多少。这个宏的定义如下:

#define EVIOCGBIT(ev,len)	_IOC(_IOC_READ, 'E', 0x20 + (ev), len)	/* get event bits */

开发板示例代码

#include <linux/input.h>      // 输入设备相关的头文件
#include <sys/types.h>        // 基本系统数据类型
#include <sys/stat.h>         // 文件状态相关函数
#include <fcntl.h>            // 文件控制选项
#include <sys/ioctl.h>        // 输入输出控制函数
#include <stdio.h>            // 标准输入输出函数/** 程序入口点* 参数:*   argc: 命令行参数个数*   argv: 命令行参数数组*/
int main(int argc, char **argv)
{int fd;                  // 文件描述符int err;                 // 错误码int len;                 // 读取长度int i;                   // 循环变量unsigned char byte;      // 用于处理位掩码的字节int bit;                 // 位索引struct input_id id;      // 输入设备ID结构体unsigned int evbit[2];   // 用于存储事件类型位掩码的数组// 事件类型名称数组char *ev_names[] = {"EV_SYN ",   // 同步事件"EV_KEY ",   // 按键事件"EV_REL ",   // 相对事件"EV_ABS ",   // 绝对事件"EV_MSC ",   // 其他事件"EV_SW ",    // 开关事件"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"EV_LED ",   // LED事件"EV_SND ",   // 声音事件"NULL ",     // 保留"EV_REP ",   // 重复事件"EV_FF ",    // 力反馈事件"EV_PWR ",   // 电源事件};// 检查命令行参数if (argc != 2){printf("Usage: %s <dev>\n", argv[0]); // 打印使用说明return -1;}// 打开指定的输入设备文件fd = open(argv[1], O_RDWR);if (fd < 0){printf("open %s err\n", argv[1]); // 打开设备失败return -1;}// 获取输入设备的ID信息err = ioctl(fd, EVIOCGID, &id);if (err == 0){printf("bustype = 0x%x\n", id.bustype ); // 打印总线类型printf("vendor  = 0x%x\n", id.vendor  ); // 打印厂商IDprintf("product = 0x%x\n", id.product ); // 打印产品IDprintf("version = 0x%x\n", id.version ); // 打印版本号}// 获取设备支持的事件类型位掩码len = ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);if (len > 0 && len <= sizeof(evbit)){printf("support ev type: "); // 打印支持的事件类型for (i = 0; i < len; i++){byte = ((unsigned char *)evbit)[i]; // 获取当前字节for (bit = 0; bit < 8; bit++){if (byte & (1<<bit)) { // 检查当前位是否为1printf("%s ", ev_names[i*8 + bit]); // 打印对应的事件类型名称}}}printf("\n");}// 关闭设备文件close(fd);return 0;
}

开发板示例代码分析

使用ioctl获取device ID时,参考驱动程序evdev_do_ioctl,需要一个input_id结构体存放device ID的信息:

在这里插入图片描述

ioctl中需要使用EVIOCGBIT来获取evbit,分析驱动程序evdev_do_ioctl,_IOC_READ读取信息调用handle_eviocgbit函数中的case 0最终获取evbit:

在这里插入图片描述

最后把evbit中读到的数据和ev_names(ev_names中定义的是时间类型type,参考:2.2 APP从输入设备获取的数据类型结构体)中的字符串进行比较,打印出支持的event类型。

上机实验

3.3.2 查询方式(Polling)和休眠唤醒方式(Blocking I/O)获取设备输入的信息
  • 查询方式定义

    • 查询方式:在查询方式中,进程不断地检查设备的状态,以确定是否有数据可以读取或写入。这种方式通常通过循环来实现。
  • 特点

    • 主动检查:进程主动检查设备状态,不依赖于外部事件。

    • CPU占用高:频繁的检查会导致CPU占用率高,不适合高负载应用。

    • 实时性:可以实现较高的实时性,因为不需要等待外部中断。

    • 查询方式读取数据通用示例代码

      #include <unistd.h>
      #include <fcntl.h>
      #include <stdio.h>int main() {int fd = open("/dev/mydevice", O_RDONLY);if (fd < 0) {perror("open");return 1;}char buffer[1024];while (1) {ssize_t bytes_read = read(fd, buffer, sizeof(buffer));if (bytes_read > 0) {// 处理读取的数据printf("Read %zd bytes\n", bytes_read);} else if (bytes_read == 0) {// 设备关闭break;} else {perror("read");break;}// 短暂休眠以减少CPU占用usleep(1000);}close(fd);return 0;
      }
      
  • 休眠唤醒方式定义

    • 休眠唤醒方式:在休眠唤醒方式中,进程在没有数据可读或写时进入休眠状态,等待设备产生中断或数据准备好后再被唤醒。这种方式通常通过阻塞I/O操作实现。
  • 特点

    • 被动等待:进程在没有数据可读或写时进入休眠状态,不占用CPU资源。

    • CPU占用低:只有在数据准备好时才占用CPU资源。

    • 实时性较低:需要等待设备中断或内核调度,实时性不如查询方式。

    • 休眠唤醒方式读取数据通用示例代码示例代码

      #include <unistd.h>
      #include <fcntl.h>
      #include <stdio.h>int main() {int fd = open("/dev/mydevice", O_RDONLY);if (fd < 0) {perror("open");return 1;}char buffer[1024];while (1) {ssize_t bytes_read = read(fd, buffer, sizeof(buffer));if (bytes_read > 0) {// 处理读取的数据printf("Read %zd bytes\n", bytes_read);} else if (bytes_read == 0) {// 设备关闭break;} else {perror("read");break;}}close(fd);return 0;
      }
      

开发板示例代码

#include <linux/input.h>      // 输入设备相关的头文件
#include <sys/types.h>        // 基本系统数据类型
#include <sys/stat.h>         // 文件状态相关函数
#include <fcntl.h>            // 文件控制选项
#include <sys/ioctl.h>        // 输入输出控制函数
#include <stdio.h>            // 标准输入输出函数
#include <string.h>           // 字符串操作函数
#include <unistd.h>           // UNIX标准函数/* * 程序入口点* 参数:*   argc: 命令行参数个数*   argv: 命令行参数数组*/
int main(int argc, char **argv)
{int fd;                  // 文件描述符int err;                 // 错误码int len;                 // 读取长度int i;                   // 循环变量unsigned char byte;      // 用于处理位掩码的字节int bit;                 // 位索引struct input_id id;      // 输入设备ID结构体unsigned int evbit[2];   // 用于存储事件类型位掩码的数组struct input_event event;// 输入事件结构体// 事件类型名称数组char *ev_names[] = {"EV_SYN ",   // 同步事件"EV_KEY ",   // 按键事件"EV_REL ",   // 相对事件"EV_ABS ",   // 绝对事件"EV_MSC ",   // 其他事件"EV_SW ",    // 开关事件"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"EV_LED ",   // LED事件"EV_SND ",   // 声音事件"NULL ",     // 保留"EV_REP ",   // 重复事件"EV_FF ",    // 力反馈事件"EV_PWR ",   // 电源事件};// 检查命令行参数if (argc < 2){printf("Usage: %s <dev> [noblock]\n", argv[0]); // 打印使用说明return -1;}// 检查是否指定了非阻塞模式if (argc == 3 && !strcmp(argv[2], "noblock")){fd = open(argv[1], O_RDWR | O_NONBLOCK); // 以读写和非阻塞模式打开设备}else{fd = open(argv[1], O_RDWR); // 以读写模式打开设备}if (fd < 0){printf("open %s err\n", argv[1]); // 打开设备失败return -1;}// 获取输入设备的ID信息err = ioctl(fd, EVIOCGID, &id);if (err == 0){printf("bustype = 0x%x\n", id.bustype ); // 打印总线类型printf("vendor  = 0x%x\n", id.vendor  ); // 打印厂商IDprintf("product = 0x%x\n", id.product ); // 打印产品IDprintf("version = 0x%x\n", id.version ); // 打印版本号}// 获取设备支持的事件类型位掩码len = ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);if (len > 0 && len <= sizeof(evbit)){printf("support ev type: "); // 打印支持的事件类型for (i = 0; i < len; i++){byte = ((unsigned char *)evbit)[i]; // 获取当前字节for (bit = 0; bit < 8; bit++){if (byte & (1<<bit)) { // 检查当前位是否为1printf("%s ", ev_names[i*8 + bit]); // 打印对应的事件类型名称}}}printf("\n");}// 无限循环读取并处理输入事件while (1){len = read(fd, &event, sizeof(event)); // 读取一个输入事件if (len == sizeof(event)){printf("get event: type = 0x%x, code = 0x%x, value = 0x%x\n", event.type, event.code, event.value); // 打印事件信息}else{printf("read err %d\n", len); // 读取错误}}return 0;
}

开发板示例代码分析

相比使用ioctl获取设备信息的代码相比,在open文件时多了O_NONBLOCK的判断:

使用O_NONBLOCK方式open文件时,表示“非阻塞”,APP调用read函数读取数据时,如果驱动程序中有数据,那么APP的read函数会返回数据,否则也会立刻返回错误;

不使用O_NONBLOCK方式open文件时,表示“阻塞”,APP调用read函数读取数据时,如果驱动程序中有数据,那么APP的read函数会返回数据;否则APP就会在内核态休眠,当有数据时驱动程序会把APP唤醒,read函数恢复执行并返回数据给APP。

通过分析驱动程序evdev_read:

在这里插入图片描述

增加了一个input_event结构体的event变量,在驱动程序evdev_read中读取到的数据就是从input_event结构体类型的变量拷贝到用户空间的buffer:

在这里插入图片描述

上机实验

在这里插入图片描述

在这里插入图片描述

3.3.3 poll方式(Polling with poll)获取设备输入的信息
  • 定义

    • poll方式poll 是一个后来引入的多路复用I/O系统调用,旨在解决 select 的一些限制。它允许一个进程监视多个文件描述符,等待其中任何一个文件描述符变为可读、可写或发生异常。
  • 函数原型

    • #include <poll.h>
      int poll(struct pollfd *fds, nfds_t nfds, int timeout);
      
  • 参数

    • fds:一个 struct pollfd 数组,每个元素包含一个文件描述符及其感兴趣的事件。

    • nfds:数组中元素的数量。

      struct pollfd {int fd;       // 文件描述符short events; // 要监视的事件short revents;// 实际发生的事件
      };
      
    • timeout:等待的超时时间,以毫秒为单位,可以是 -1 表示无限期等待。

      注:**fds**中events有多种类型,如下图:

      在这里插入图片描述

  • 特点

    • 动态大小的文件描述符集合poll 使用 struct pollfd 数组来表示文件描述符集合,数组的大小是动态的,没有固定的上限。

    • 不需要重新初始化poll 不会修改传入的 struct pollfd 数组,因此不需要在每次调用前重新初始化。

    • 更好的性能poll 在处理大量文件描述符时性能更好,因为它只需要遍历实际使用的文件描述符,而不是固定的集合。

    • poll方式读取数据通用示例代码示例代码

      #include <poll.h>
      #include <unistd.h>
      #include <stdio.h>int main() {int fd1 = 0; // 标准输入int fd2 = 1; // 标准输出struct pollfd fds[1];fds[0].fd = fd1;fds[0].events = POLLIN;int ret = poll(fds, 1, 5000); // 5000毫秒超时if (ret < 0) {perror("poll");return 1;} else if (ret == 0) {printf("Timeout occurred! No data after 5 seconds.\n");} else {if (fds[0].revents & POLLIN) {char buffer[1024];ssize_t bytes_read = read(fd1, buffer, sizeof(buffer));if (bytes_read > 0) {printf("Read %zd bytes: %.*s\n", bytes_read, (int)bytes_read, buffer);}}}return 0;
      }
      

开发板示例代码

#include <linux/input.h>      // 输入设备相关的头文件
#include <sys/types.h>        // 基本系统数据类型
#include <sys/stat.h>         // 文件状态相关函数
#include <fcntl.h>            // 文件控制选项
#include <sys/ioctl.h>        // 输入输出控制函数
#include <stdio.h>            // 标准输入输出函数
#include <string.h>           // 字符串操作函数
#include <unistd.h>           // UNIX标准函数
#include <poll.h>             // poll相关函数和结构体/** 获取输入设备信息并持续监听事件* 参数:*   argc: 命令行参数个数*   argv: 命令行参数数组*/
int main(int argc, char **argv)
{int fd;                  // 文件描述符int err;                 // 错误码int len;                 // 读取长度int ret;                 // poll返回值int i;                   // 循环变量unsigned char byte;      // 用于处理位掩码的字节int bit;                 // 位索引struct input_id id;      // 输入设备ID结构体unsigned int evbit[2];   // 用于存储事件类型位掩码的数组struct input_event event; // 输入事件结构体struct pollfd fds[1];    // pollfd结构体数组nfds_t nfds = 1;         // 监视的文件描述符数量// 事件类型名称数组char *ev_names[] = {"EV_SYN ",   // 同步事件"EV_KEY ",   // 按键事件"EV_REL ",   // 相对事件"EV_ABS ",   // 绝对事件"EV_MSC ",   // 其他事件"EV_SW ",    // 开关事件"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"EV_LED ",   // LED事件"EV_SND ",   // 声音事件"NULL ",     // 保留"EV_REP ",   // 重复事件"EV_FF ",    // 力反馈事件"EV_PWR ",   // 电源事件};// 检查命令行参数if (argc != 2){printf("Usage: %s <dev>\n", argv[0]); // 打印使用说明return -1;}// 打开指定的输入设备文件,使用非阻塞模式fd = open(argv[1], O_RDWR | O_NONBLOCK);if (fd < 0){printf("open %s err\n", argv[1]); // 打开设备失败return -1;}// 获取输入设备的ID信息err = ioctl(fd, EVIOCGID, &id);if (err == 0){printf("bustype = 0x%x\n", id.bustype ); // 打印总线类型printf("vendor  = 0x%x\n", id.vendor  ); // 打印厂商IDprintf("product = 0x%x\n", id.product ); // 打印产品IDprintf("version = 0x%x\n", id.version ); // 打印版本号}// 获取设备支持的事件类型位掩码len = ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);if (len > 0 && len <= sizeof(evbit)){printf("support ev type: "); // 打印支持的事件类型for (i = 0; i < len; i++){byte = ((unsigned char *)evbit)[i]; // 获取当前字节for (bit = 0; bit < 8; bit++){if (byte & (1 << bit)) { // 检查当前位是否为1printf("%s ", ev_names[i * 8 + bit]); // 打印对应的事件类型名称}}}printf("\n");}// 持续监听事件while (1){fds[0].fd = fd;                  // 设置文件描述符fds[0].events = POLLIN;          // 设置要监视的事件(可读)fds[0].revents = 0;              // 清除实际发生的事件ret = poll(fds, nfds, 5000);     // 调用poll,等待5000毫秒if (ret > 0){if (fds[0].revents & POLLIN) // 检查是否发生可读事件{while (read(fd, &event, sizeof(event)) == sizeof(event)) // 读取事件{printf("get event: type = 0x%x, code = 0x%x, value = 0x%x\n", event.type, event.code, event.value); // 打印事件信息}}}else if (ret == 0){printf("time out\n"); // 超时}else{printf("poll err\n"); // poll出错}}return 0;
}

开发板示例代码分析

分析驱动程序evdev_poll主要是将文件描述符添加到等待队列中,以便在文件描述符的状态发生变化时,能够通知调用者:

在这里插入图片描述

在这里插入图片描述

上机实验

在这里插入图片描述

3.3.4 selsct方式(Polling with select)获取设备输入的信息
  • 定义

    • select方式select 是一个早期的多路复用I/O系统调用,广泛应用于Unix和类Unix系统中。它允许一个进程监视多个文件描述符,等待其中任何一个文件描述符变为可读、可写或发生异常。
  • 函数原型

    • #include <sys/select.h>
      int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
      
  • 参数

    • nfds:要监视的最大文件描述符值加1。
    • readfds:要监视的可读文件描述符集合。
    • writefds:要监视的可写文件描述符集合。
    • exceptfds:要监视的异常条件文件描述符集合。
    • timeout:等待的超时时间,可以是 NULL 表示无限期等待。
  • 特点

    • 固定大小的文件描述符集合select 使用 fd_set 结构来表示文件描述符集合,fd_set 的大小是固定的,通常是1024个文件描述符。

    • 需要重新初始化:每次调用 select 之前,都需要重新初始化 fd_set,因为 select 会修改这些集合。

    • 性能问题:随着文件描述符数量的增加,select 的性能会下降,因为它需要遍历整个 fd_set 来检查每个文件描述符的状态。

    • select方式读取数据通用示例代码示例代码

      #include <sys/select.h>
      #include <unistd.h>
      #include <stdio.h>int main() {int fd1 = 0; // 标准输入int fd2 = 1; // 标准输出fd_set readfds;FD_ZERO(&readfds);FD_SET(fd1, &readfds);struct timeval timeout;timeout.tv_sec = 5;timeout.tv_usec = 0;int ret = select(fd1 + 1, &readfds, NULL, NULL, &timeout);if (ret < 0) {perror("select");return 1;} else if (ret == 0) {printf("Timeout occurred! No data after 5 seconds.\n");} else {if (FD_ISSET(fd1, &readfds)) {char buffer[1024];ssize_t bytes_read = read(fd1, buffer, sizeof(buffer));if (bytes_read > 0) {printf("Read %zd bytes: %.*s\n", bytes_read, (int)bytes_read, buffer);}}}return 0;
      }
      

开发板示例代码

#include <linux/input.h>      // 输入设备相关的头文件
#include <sys/types.h>        // 基本系统数据类型
#include <sys/stat.h>         // 文件状态相关函数
#include <fcntl.h>            // 文件控制选项
#include <sys/ioctl.h>        // 输入输出控制函数
#include <stdio.h>            // 标准输入输出函数
#include <string.h>           // 字符串操作函数
#include <unistd.h>           // UNIX标准函数
#include <sys/select.h>       // select相关函数和结构体
#include <sys/time.h>         // 时间相关的头文件/** 获取输入设备信息并持续监听事件* 参数:*   argc: 命令行参数个数*   argv: 命令行参数数组*/
int main(int argc, char **argv)
{int fd;                  // 文件描述符int err;                 // 错误码int len;                 // 读取长度int ret;                 // select返回值int i;                   // 循环变量unsigned char byte;      // 用于处理位掩码的字节int bit;                 // 位索引struct input_id id;      // 输入设备ID结构体unsigned int evbit[2];   // 用于存储事件类型位掩码的数组struct input_event event; // 输入事件结构体int nfds;                // 最大的文件描述符加1struct timeval tv;       // 超时时间fd_set readfds;          // 用于select的读文件描述符集合// 事件类型名称数组char *ev_names[] = {"EV_SYN ",   // 同步事件"EV_KEY ",   // 按键事件"EV_REL ",   // 相对事件"EV_ABS ",   // 绝对事件"EV_MSC ",   // 其他事件"EV_SW ",    // 开关事件"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"EV_LED ",   // LED事件"EV_SND ",   // 声音事件"NULL ",     // 保留"EV_REP ",   // 重复事件"EV_FF ",    // 力反馈事件"EV_PWR ",   // 电源事件};// 检查命令行参数if (argc != 2){printf("Usage: %s <dev>\n", argv[0]); // 打印使用说明return -1;}// 打开指定的输入设备文件,使用非阻塞模式fd = open(argv[1], O_RDWR | O_NONBLOCK);if (fd < 0){printf("open %s err\n", argv[1]); // 打开设备失败return -1;}// 获取输入设备的ID信息err = ioctl(fd, EVIOCGID, &id);if (err == 0){printf("bustype = 0x%x\n", id.bustype ); // 打印总线类型printf("vendor  = 0x%x\n", id.vendor  ); // 打印厂商IDprintf("product = 0x%x\n", id.product ); // 打印产品IDprintf("version = 0x%x\n", id.version ); // 打印版本号}// 获取设备支持的事件类型位掩码len = ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);if (len > 0 && len <= sizeof(evbit)){printf("support ev type: "); // 打印支持的事件类型for (i = 0; i < len; i++){byte = ((unsigned char *)evbit)[i]; // 获取当前字节for (bit = 0; bit < 8; bit++){if (byte & (1 << bit)) { // 检查当前位是否为1printf("%s ", ev_names[i * 8 + bit]); // 打印对应的事件类型名称}}}printf("\n");}// 持续监听事件while (1){// 设置超时时间tv.tv_sec  = 5;     // 超时时间为5秒tv.tv_usec = 0;     // 微秒部分为0// 初始化读文件描述符集合FD_ZERO(&readfds);  // 先全部清零FD_SET(fd, &readfds); // 将文件描述符fd添加到读集合中// 设置最大的文件描述符加1nfds = fd + 1;      // nfds 是最大的文件描述符加1// 调用select,等待文件描述符变为可读或超时ret = select(nfds, &readfds, NULL, NULL, &tv);if (ret > 0)  // 有文件描述符准备好{// 再次确认fd有数据if (FD_ISSET(fd, &readfds)){// 读取并处理事件while (read(fd, &event, sizeof(event)) == sizeof(event)){printf("get event: type = 0x%x, code = 0x%x, value = 0x%x\n", event.type, event.code, event.value); // 打印事件信息}}}else if (ret == 0)  // 超时{printf("time out\n"); // 打印超时信息}else   // -1: error{printf("select err\n"); // 打印错误信息}}return 0;
}

开发板示例代码分析

在内核空间中,驱动程序需要支持 select,通常通过实现 poll 方法来实现。poll 方法是 select 的底层实现,允许内核知道哪些文件描述符已经准备好进行I/O操作。参考poll函数的代码分析。

上机实验

在这里插入图片描述

3.3.5 异步通知方式(Asynchronous I/O)获取设备输入的信息
  • 定义

    • 异步通知方式:在异步通知方式中,进程发起I/O请求后立即返回,继续执行其他任务。当I/O操作完成时,系统通过回调函数或信号等方式通知进程。这种方式通常通过aio(Asynchronous I/O)库实现。
  • 函数原型

    • #include <signal.h>
      typedef void (*sighandler_t)(int);
      sighandler_t signal(int signum, sighandler_t handler);
      
  • 参数

    • signum:指定要处理的信号编号。信号编号是一个整数,用于唯一标识一个信号。例如,SIGINT表示中断信号(通常由Ctrl+C产生)。

      在这里插入图片描述

    • handler:是一个指向函数的指针,该函数用于处理指定的信号。这个函数应该接受一个整型参数(即信号编号),并且没有返回值(即返回类型为void)。当信号signum发生时,系统将调用这个函数。

    • 返回值:如果调用成功,signal函数返回之前的信号处理函数指针;如果调用失败,返回SIG_ERR

  • 信号处理函数

    • handler参数可以指向以下几种类型的处理函数:
      • 用户自定义的处理函数:程序员可以编写自己的信号处理函数,并将其地址作为handler参数传递给signal函数。当信号发生时,系统将调用这个函数。
      • 忽略信号:通过将handler设置为SIG_IGN,可以告诉系统忽略指定的信号。但是,请注意,有些信号是不能被忽略的,如SIGKILLSIGSTOP
      • 采用默认处理:通过将handler设置为SIG_DFL,可以告诉系统采用默认的信号处理机制。对于大多数信号来说,默认的处理机制是终止进程。
  • 特点

    • 非阻塞:进程发起I/O请求后立即返回,不等待I/O操作完成。

    • 高效:适合高并发和高性能应用,可以充分利用CPU资源。

    • 复杂:需要处理回调函数或信号,编程复杂度较高。

    • 异步通知方式使用single函数读取数据通用示例代码示例代码

      #include <stdio.h>
      #include <stdlib.h>
      #include <string.h>
      #include <unistd.h>
      #include <fcntl.h>
      #include <sys/types.h>
      #include <sys/stat.h>
      #include <linux/input.h>
      #include <signal.h>/* 信号处理函数 */
      void handle_sigio(int signum) {static struct input_event event;int fd = fileno(stdin); // 假设我们监听的是标准输入,实际应用中应替换为设备文件描述符// 读取事件ssize_t bytes_read = read(fd, &event, sizeof(event));if (bytes_read == sizeof(event)) {printf("get event: type = 0x%x, code = 0x%x, value = 0x%x\n", event.type, event.code, event.value);} else if (bytes_read == -1) {perror("read");} else {printf("Partial read: %zd bytes\n", bytes_read);}
      }/* 主函数 */
      int main(int argc, char **argv) {int fd;struct sigaction sa;if (argc != 2) {fprintf(stderr, "Usage: %s <device>\n", argv[0]);return -1;}// 打开设备文件fd = open(argv[1], O_RDONLY | O_NONBLOCK);if (fd == -1) {perror("open");return -1;}// 设置文件描述符的所有者fcntl(fd, F_SETOWN, getpid());// 设置文件描述符为异步通知fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_ASYNC);// 注册信号处理函数memset(&sa, 0, sizeof(sa));sa.sa_handler = handle_sigio;sa.sa_flags = 0;if (sigaction(SIGIO, &sa, NULL) == -1) {perror("sigaction");return -1;}// 主循环while (1) {pause(); // 等待信号}close(fd);return 0;
      }
      

开发板示例代码

#include <linux/input.h>      // 输入设备相关的头文件
#include <sys/types.h>        // 基本系统数据类型
#include <sys/stat.h>         // 文件状态相关函数
#include <fcntl.h>            // 文件控制选项
#include <sys/ioctl.h>        // 输入输出控制函数
#include <stdio.h>            // 标准输入输出函数
#include <string.h>           // 字符串操作函数
#include <unistd.h>           // UNIX标准函数
#include <signal.h>           // 信号处理相关函数int fd;                      // 文件描述符// 信号处理函数
void my_sig_handler(int sig)
{struct input_event event; // 输入事件结构体// 读取并处理所有可用的输入事件while (read(fd, &event, sizeof(event)) == sizeof(event)){printf("get event: type = 0x%x, code = 0x%x, value = 0x%x\n", event.type, event.code, event.value);}
}/* ./05_input_read_fasync /dev/input/event0 */
int main(int argc, char **argv)
{int err;                 // 错误码int len;                 // 读取长度int ret;                 // 通用返回值int i;                   // 循环变量unsigned char byte;      // 用于处理位掩码的字节int bit;                 // 位索引struct input_id id;      // 输入设备ID结构体unsigned int evbit[2];   // 用于存储事件类型位掩码的数组unsigned int flags;      // 文件描述符标志int count = 0;           // 计数器// 事件类型名称数组char *ev_names[] = {"EV_SYN ",   // 同步事件"EV_KEY ",   // 按键事件"EV_REL ",   // 相对事件"EV_ABS ",   // 绝对事件"EV_MSC ",   // 其他事件"EV_SW ",    // 开关事件"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"NULL ",     // 保留"EV_LED ",   // LED事件"EV_SND ",   // 声音事件"NULL ",     // 保留"EV_REP ",   // 重复事件"EV_FF ",    // 力反馈事件"EV_PWR ",   // 电源事件};// 检查命令行参数if (argc != 2){printf("Usage: %s <dev>\n", argv[0]); // 打印使用说明return -1;}// 注册信号处理函数signal(SIGIO, my_sig_handler); // 注册SIGIO信号处理函数// 打开驱动程序fd = open(argv[1], O_RDWR | O_NONBLOCK); // 以读写模式和非阻塞模式打开设备文件if (fd < 0){printf("open %s err\n", argv[1]); // 打开设备失败return -1;}// 获取输入设备的ID信息err = ioctl(fd, EVIOCGID, &id); // 使用ioctl获取设备ID信息if (err == 0){printf("bustype = 0x%x\n", id.bustype ); // 打印总线类型printf("vendor  = 0x%x\n", id.vendor  ); // 打印厂商IDprintf("product = 0x%x\n", id.product ); // 打印产品IDprintf("version = 0x%x\n", id.version ); // 打印版本号}// 获取设备支持的事件类型位掩码len = ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit); // 使用ioctl获取事件类型位掩码if (len > 0 && len <= sizeof(evbit)){printf("support ev type: "); // 打印支持的事件类型for (i = 0; i < len; i++){byte = ((unsigned char *)evbit)[i]; // 获取当前字节for (bit = 0; bit < 8; bit++){if (byte & (1 << bit)) { // 检查当前位是否为1printf("%s ", ev_names[i * 8 + bit]); // 打印对应的事件类型名称}}}printf("\n");}// 把APP的进程号告诉驱动程序fcntl(fd, F_SETOWN, getpid()); // 设置文件描述符的所有者为当前进程// 使能"异步通知"flags = fcntl(fd, F_GETFL); // 获取文件描述符的当前标志fcntl(fd, F_SETFL, flags | FASYNC); // 设置文件描述符为异步通知模式// 主循环while (1){printf("main loop count = %d\n", count++); // 打印主循环计数sleep(2); // 暂停2秒}return 0;
}

开发板示例代码分析

需要补充fcntl函数知识:

  • 函数原型

    • #include <unistd.h>
      #include <fcntl.h>
      int fcntl(int fd, int cmd, ...); /* 可变参数,根据cmd的值决定是否需要第三个参数 */
      
  • 参数

    • fd:文件描述符,表示要操作的文件。
    • cmd:操作命令,决定fcntl函数的具体行为。
    • ...:根据cmd的值,可能需要一个额外的参数,该参数的类型可以是longstruct flock *
  • 功能(包括但不限于)

    • 复制文件描述符:使用F_DUPFD命令可以复制一个现有的文件描述符。
    • 获取/设置文件描述符标记:通过F_GETFDF_SETFD命令,可以获取或设置文件描述符的标记,如FD_CLOEXEC
    • 获取/设置文件状态标记:使用F_GETFLF_SETFL命令,可以获取或设置文件的状态标记,如O_APPENDO_NONBLOCK等。
    • 获取/设置异步I/O所有权:通过F_GETOWNF_SETOWN命令,可以获取或设置接收SIGIO或SIGURG信号的进程ID或进程组ID。
    • 获取/设置记录锁:使用F_GETLKF_SETLKF_SETLKW命令,可以获取、设置或测试文件的记录锁。

上机实验

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/14848.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

解决SpringBoot3的Validated依赖实现自定义注解失效问题

我们引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency> Validated实现自定义注解 我们首先看看自定义注解里面&#xff0c;用到的注解的包 我们…

开箱即用的frp教程及脚本

废话不多说直接上干货 FRP官方&#xff1a;https://github.com/fatedier/frp FRP中文网站&#xff1a;https://gofrp.org/zh-cn/ 准备环境&#xff1a; 带公网的VPS内网Linux主机 一、在VPS上下载frp安装包 这里下载最新版的0.61.0 wget -P /frps https://github.com/fa…

程序员做自媒体,你所不了解的提词器,原来还有这么多大用处

程序猿的出路&#xff0c;不只是外卖员&#xff01; 你或许以为提词器只是个“背词神器”&#xff1f;实际上&#xff0c;它的应用范围早已超出你的想象。从直播到会议&#xff0c;从视频拍摄到户外采访&#xff0c;每一种场景都有对应的提词神器&#xff0c;帮你提升效率、避…

三种单例实现

1、不继承Mono的单例 实现 使用 注&#xff1a; 使用需要继承BaseManager 泛型填写自己本身 需要实现无参构造函数 2、挂载式的Mono单例 实现 使用 注&#xff1a; 使用需要继承SingletonMono 泛型填写自己本身 需要挂载在unity引擎面板 3、不用挂载式的单例 实现 使…

DAY112代码审计PHP开发框架POP链利用Yii反序列化POP利用链

一、pop1链的跟踪 1、路由关系 2、漏洞触发口unserialize(base64_decode($data)); 2、__destruct()&#xff0c;魔术法方法调用close函数方法 3、未找到利用链&#xff0c;尝试__call魔术方法 4、逆推找call_user_func 函数 第一部分 namespace yii\db; class BatchQueryResu…

移动硬盘需要格式化才能打开?详解原因与数据恢复方案

描述移动硬盘需要格式化才能打开 当我们尝试访问移动硬盘时&#xff0c;有时会遇到系统提示“需要格式化才能打开”的情况。这种提示通常意味着硬盘上的文件系统已损坏或无法被系统正常识别。一旦遇到这种情况&#xff0c;很多用户会感到焦虑&#xff0c;因为硬盘中可能存储了…

Java项目实战II基于微信小程序的移动学习平台的设计与实现(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 随着城市化…

项目技术栈-解决方案-注册中心

项目技术栈-解决方案-注册中心 Zookeeper、Eureka、Nacos、Consul和Etcd参考文章 服务注册中心&#xff08;Registry&#xff09;&#xff1a;用于保存 RPC Server 的注册信息&#xff0c;当 RPC Server 节点发生变更时&#xff0c;Registry 会同步变更&#xff0c;RPC Client …

力扣 LeetCode 454. 四数相加II(Day3:哈希表)

解题思路&#xff1a; 使用map 四个数组两两一组 前两个数组的各个值遍历相加&#xff0c;和为key&#xff0c;出现的次数为value 后两个数组的各个值遍历相加&#xff0c;如果该值的负数能在map中找到&#xff08;表示能抵消为0&#xff0c;符合题意四数之和为0&#xff0…

【项目组件】第三方库——websocketpp

目录 第三方协议&#xff1a;websocket websocket简介 websocket特点 websocket协议切换 websocket协议格式段 websocketpp库介绍 endpoint server connection websocketpp库搭建服务器流程 基本框架实现 业务处理回调函数的实现 http_callback open_callback …

现代电商解决方案:Spring Boot框架实践

1 绪论 1.1 研究背景 当前社会各行业领域竞争压力非常大&#xff0c;随着当前时代的信息化&#xff0c;科学化发展&#xff0c;让社会各行业领域都争相使用新的信息技术&#xff0c;对行业内的各种相关数据进行科学化&#xff0c;规范化管理。这样的大环境让那些止步不前&#…

unity3d————协程原理讲解

1.协程的本质 协程可以分成两部分1.协程函数本体 2.协程调度器 协程本体就是一个能够中间暂停返回的函数 协程调度器是Unity内部实现的&#xff0c;会在对应的时机帮助我们继续执行协程函数 Unity只实现了协程调度部分协程的本体本质上就是一个 C#的迭代器方法 2.协程本体是…

社区物资交易互助平台(程序+数据库+报告)

基于Spring Boot框架实现的社区物资交易互助平台&#xff0c;系统包含两种角色&#xff1a;管理员、用户,系统分为前台和后台两大模块&#xff0c;主要功能如下。 【前台】&#xff1a; - 首页&#xff1a;展示平台的概览信息和热门内容。 - 论坛&#xff1a;提供一个交流讨论…

学者观察 | 元计算、人工智能和Web 3.0——山东大学教授成秀珍

导语 成秀珍教授提出元计算是在开放的零信任环境下整合算力资源打通数据壁垒构建自进化智能的新质生产力技术&#xff0c;是一种新计算范式&#xff1b;区块链是Web3.0的核心技术之一&#xff0c;有助于保障开放零信任环境下&#xff0c;用户、设备和服务间去中心化数据流通的…

JMeter中添加请求头

在JMeter中添加请求头的步骤如下&#xff1a; 1.打开HTTP信息头管理器 &#xff1a; 首先&#xff0c;你需要进入JMeter的HTTP请求组件。这可以通过在HTTP请求测试元素上右键点击&#xff0c;然后选择“添加 > 配置元件 > HTTP信息头管理器”来完成。 2.添加新的请求头…

ROS Action

在 ROS 中&#xff0c;Action 是一种支持长时间异步任务的通信机制。与 Service 不同&#xff0c;Action 允许客户端发起一个请求&#xff0c;并在任务执行的过程中不断接收反馈&#xff0c;直到任务完成。这种机制非常适用于可能需要较长时间来完成的任务&#xff0c;比如机器…

江苏省考公务员报名照片要求及处理方法

随着江苏省公务员考试的临近&#xff0c;许多考生已经开始准备报名所需的各项材料&#xff0c;其中照片的准备尤为重要。本文将详细介绍江苏省考公务员报名照片的具体要求以及如何使用手机拍照并处理照片&#xff0c;确保您的报名过程顺利进行。 一、江苏省公务员招录考试报名照…

计算机网络学习笔记-3.2介质访问控制

文章目录 介质访问控制静态划分信道 动态分配信道轮询访问介质访问控制随机访问介质访问控制ALOHA协议简介ALOHA协议的工作原理 介质访问控制 介质访问控制&#xff08;MAC&#xff0c;Medium Access Control&#xff09;&#xff0c;质访问控制的目的是确保多个设备能够高效、…

软件测试-巨量测试开发

软件测试-巨量测试 编辑时间&#xff1a;2024/11/13 软件测试基础知识 软件测试定义和测试分类 软件是计算机程序、程序所用的数据以及有关文档资料的集合。 软件测试分类 按测试执行阶段划分 单元测试、集成测试、系统测试、验收测试 是否运行程序划分 动态测试、静态测试…

pycharm中from[本地包]import文件/模块出现问题(最最最全方法!)

1.通过PYTHONPATH的方法在此处将路径添加上&#xff0c;能够让IDE访问得到。 2.通过选中目标文件所在的文件的文件夹单击右键&#xff0c;如下图所示可以看到下方的mark directory as选项中存在 存在excluded&#xff0c;选择此项可解决问题&#xff0c;如果仍有问题可以尝试其…