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

嵌入式Linux输入子系统驱动开发

嵌入式Linux输入子系统驱动开发

1. 输入子系统概述

Linux输入子系统是内核中专门处理输入设备的框架,它为各种输入设备(如键盘、鼠标、触摸屏、游戏手柄等)提供统一的接口和抽象层。输入子系统的主要目标是简化输入设备驱动的开发,提高代码的可重用性,并为用户空间应用程序提供一致的访问接口。

1.1 设计目标

  • 抽象化: 将不同类型的输入设备抽象为统一的接口
  • 模块化: 允许独立开发和加载不同的输入设备驱动
  • 标准化: 提供标准的事件报告机制
  • 可扩展性: 支持新类型输入设备的轻松集成

1.2 子系统组成

Linux输入子系统主要由三部分组成:

  1. 驱动层(Driver Layer): 负责与具体硬件交互,将硬件事件转换为标准输入事件
  2. 核心层(Core Layer): 提供统一的API和事件处理机制
  3. 事件处理层(Event Handler Layer): 将输入事件传递给用户空间应用程序

2. 输入子系统核心架构

2.1 核心数据结构

输入子系统的核心是input_dev结构体,它定义了输入设备的基本属性和操作方法:

struct input_dev {const char *name;const char *phys;const char *uniq;struct input_id id;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)];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 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 input_absinfo *absinfo;unsigned int rep[REP_CNT];struct timer_list timer;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 *grab;spinlock_t event_lock;struct mutex mutex;unsigned int users;bool going_away;bool sync;struct device dev;struct list_head    h_list;struct list_head    node;
};

2.2 事件类型

输入子系统定义了多种事件类型,每种类型对应不同的输入行为:

  • EV_SYN: 同步事件,用于分隔事件包
  • EV_KEY: 按键事件,包括键盘、按钮等
  • EV_REL: 相对坐标事件,如鼠标移动
  • EV_ABS: 绝对坐标事件,如触摸屏
  • EV_MSC: 杂项事件,用于传输设备特定信息
  • EV_SW: 开关事件,用于检测设备状态变化
  • EV_LED: LED事件,用于控制设备LED
  • EV_SND: 声音事件,用于控制设备声音
  • EV_REP: 重复事件,用于按键重复
  • EV_FF: 力反馈事件,用于振动反馈
  • EV_PWR: 电源事件,用于电源管理

2.3 事件编码

每种事件类型都有相应的编码,用于标识具体的输入源。例如:

  • KEY_0KEY_9: 数字键
  • KEY_AKEY_Z: 字母键
  • KEY_ENTER: 回车键
  • KEY_SPACE: 空格键
  • BTN_LEFT: 鼠标左键
  • BTN_RIGHT: 鼠标右键
  • ABS_X, ABS_Y: 绝对X、Y坐标

3. 事件类型与编码

3.1 事件类型详解

EV_KEY

按键事件是最常见的输入事件类型,用于表示按键的按下和释放。每个按键事件包含三个主要信息:

  • type: 事件类型,固定为EV_KEY
  • code: 按键编码,标识具体哪个按键
  • value: 按键状态,0表示释放,1表示按下,2表示自动重复
EV_SYN

同步事件用于分隔事件包,确保事件的原子性。当多个相关事件需要同时处理时,它们会被打包在一个事件包中,以EV_SYN事件结束。常见的同步事件包括:

  • SYN_REPORT: 表示一个完整的事件包结束
  • SYN_CONFIG: 表示设备配置发生变化
  • SYN_MT_REPORT: 多点触控事件报告

3.2 事件编码规范

Linux内核定义了标准的事件编码,确保不同设备之间的兼容性。编码遵循以下原则:

  • 可读性: 编码名称应直观反映其功能
  • 一致性: 相同类别的设备使用相似的编码方案
  • 扩展性: 预留足够的编码空间以支持新设备

4. 输入设备注册流程

4.1 设备注册步骤

输入设备的注册流程遵循以下步骤:

  1. 分配输入设备结构体
    使用input_allocate_device()函数分配input_dev结构体

  2. 设置设备属性

    • 设置设备名称name
    • 设置物理路径phys
    • 设置唯一标识uniq
  3. 设置支持的事件类型
    使用__set_bit()函数设置设备支持的事件类型位图

  4. 设置支持的按键编码
    对于按键设备,需要设置支持的按键编码

  5. 注册输入设备
    使用input_register_device()函数将设备注册到输入子系统

  6. 设备注销
    在模块卸载时,使用input_unregister_device()注销设备

4.2 代码示例

static int __init keyinput_init(void)
{int ret = 0;// 初始化按键ret = key_init(&keyinputdev);if (ret < 0) {goto fail_key_init;}// 分配输入设备keyinputdev.inputdev = input_allocate_device();if (keyinputdev.inputdev == NULL) {ret = -EINVAL;goto fail_input_allocate;}// 设置设备名称keyinputdev.inputdev->name = KEYINPUT_NAME;// 设置支持的事件类型__set_bit(EV_KEY, keyinputdev.inputdev->evbit);__set_bit(EV_REP, keyinputdev.inputdev->evbit);// 设置支持的按键__set_bit(KEY_0, keyinputdev.inputdev->keybit);// 注册输入设备ret = input_register_device(keyinputdev.inputdev);if (ret) {ret = -EINVAL;goto fail_input_reg;}return 0;fail_input_reg:input_free_device(keyinputdev.inputdev);
fail_input_allocate:
fail_key_init:return ret;
}

5. 中断处理机制

5.1 中断请求

在嵌入式系统中,按键通常通过GPIO引脚连接到处理器。当按键状态发生变化时,会触发GPIO中断。驱动程序需要注册中断处理函数来响应这些中断。

5.1.1 中断注册
ret = request_irq(dev->key[i].irqnum, dev->key[i].handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, dev->key[i].name, &keyinputdev);

参数说明:

  • irqnum: 中断号
  • handler: 中断处理函数
  • flags: 中断触发方式
    • IRQF_TRIGGER_FALLING: 下降沿触发
    • IRQF_TRIGGER_RISING: 上升沿触发
    • IRQF_TRIGGER_EDGE: 边沿触发
    • IRQF_TRIGGER_LEVEL: 电平触发
  • name: 中断名称
  • dev_id: 设备标识,用于共享中断

5.2 中断处理函数

中断处理函数应该尽可能简洁,避免长时间占用中断上下文。通常的做法是将耗时的操作推迟到下半部执行。

static irqreturn_t key0_handler(int irq, void *filp)
{struct keyinput_dev *dev = filp;dev->timer.data = (volatile long)filp;// 启动定时器进行防抖mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20));return IRQ_HANDLED;
}

5.3 中断下半部

Linux内核提供了多种机制来处理中断下半部:

  • 软中断(Softirq)
  • tasklet
  • 工作队列(Workqueue)
  • 定时器(Timer)

在本例中,使用定时器作为中断下半部,既实现了防抖功能,又完成了按键状态的检测。

6. 定时器防抖技术

6.1 按键抖动问题

机械按键在按下和释放时会产生电气抖动,导致短时间内多次触发中断。如果不进行处理,会导致按键事件被错误地报告多次。

6.2 防抖解决方案

常见的防抖方法包括:

  1. 硬件防抖: 使用RC电路或施密特触发器
  2. 软件防抖: 使用延时检测

本驱动采用软件防抖方案,通过定时器实现:

static void timer_func(unsigned long arg)
{struct keyinput_dev *dev = (struct keyinput_dev *)arg;int value = 0;value = gpio_get_value(dev->key[0].gpio);if (value == 0){input_event(dev->inputdev, EV_KEY, KEY_0, 1);input_sync(dev->inputdev);}else{input_event(dev->inputdev, EV_KEY, KEY_0, 0);input_sync(dev->inputdev);}
}

6.3 防抖参数选择

防抖时间的选择需要平衡响应速度和可靠性:

  • 过短: 无法有效消除抖动
  • 过长: 影响用户体验

通常,机械按键的抖动时间为5-20ms,因此选择20ms作为防抖时间是合理的。

7. 设备树(DTS)配置

7.1 设备树概述

设备树(Device Tree)是一种描述硬件配置的数据结构,它将硬件信息从内核代码中分离出来,提高了代码的可移植性和可维护性。

7.2 按键节点配置

key{compatible = "alientek,key";pinctrl-names = "default";pinctrl-0 = <&pinctrl_key>;states = "okay";key-gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>;interrupt-parent = <&gpio1>;interrupts = <18 IRQ_TYPE_EDGE_BOTH>;
};
7.2.1 属性说明
  • compatible: 兼容性字符串,用于匹配驱动程序
  • pinctrl-names: 引脚控制状态名称
  • pinctrl-0: 默认状态下的引脚配置
  • key-gpios: GPIO引脚配置
    • &gpio1: GPIO控制器
    • 18: GPIO编号
    • GPIO_ACTIVE_HIGH: 高电平有效
  • interrupt-parent: 中断父节点
  • interrupts: 中断配置
    • 18: 中断号
    • IRQ_TYPE_EDGE_BOTH: 边沿触发(上升沿和下降沿)

7.3 引脚控制配置

pinctrl_key: keygrp {fsl,pins = <MX6UL_PAD_UART1_CTS_B__GPIO1_IO18    0xF080>;
};
7.3.1 引脚配置参数
  • MX6UL_PAD_UART1_CTS_B__GPIO1_IO18: 引脚复用配置
  • 0xF080: 电气特性配置
    • 驱动强度
    • 上拉/下拉电阻
    • 压摆率等

8. 驱动代码详细分析

8.1 头文件包含

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/string.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/input.h>
8.1.1 头文件功能
  • linux/module.h: 模块相关定义
  • linux/kernel.h: 内核常用宏和函数
  • linux/init.h: 初始化相关宏
  • linux/fs.h: 文件系统相关定义
  • asm/io.h: I/O操作函数
  • asm/uaccess.h: 用户空间访问函数
  • linux/cdev.h: 字符设备相关定义
  • linux/device.h: 设备模型相关定义
  • linux/of.h: 设备树相关定义
  • linux/of_address.h: 设备树地址相关定义
  • linux/slab.h: 内存分配函数
  • linux/gpio.h: GPIO操作函数
  • linux/of_gpio.h: 设备树GPIO操作函数
  • linux/string.h: 字符串操作函数
  • linux/interrupt.h: 中断相关定义
  • linux/irq.h: IRQ相关定义
  • linux/input.h: 输入子系统相关定义

8.2 宏定义

#define KEYINPUT_CNT 1
#define KEYINPUT_NAME "keyinputdev"
#define KEY0VALUE 0x10
#define KEYINVA 0xff
#define KEY_NUM 1
8.2.1 宏说明
  • KEYINPUT_CNT: 设备计数
  • KEYINPUT_NAME: 设备名称
  • KEY0VALUE: 按键值
  • KEYINVA: 无效按键值
  • KEY_NUM: 按键数量

8.3 数据结构定义

8.3.1 key_desc结构体
struct key_desc
{char name[10];int gpio;int irqnum;unsigned char value;irqreturn_t (*handler)(int, void *);
};

该结构体描述单个按键的属性:

  • name: 按键名称
  • gpio: GPIO编号
  • irqnum: 中断号
  • value: 按键值
  • handler: 中断处理函数
8.3.2 keyinput_dev结构体
struct keyinput_dev
{struct device_node *key_nd;struct key_desc key[KEY_NUM];struct timer_list timer;struct input_dev *inputdev;
};

该结构体描述整个输入设备:

  • key_nd: 设备树节点
  • key: 按键数组
  • timer: 定时器
  • inputdev: 输入设备结构体

8.4 函数实现

8.4.1 中断处理函数
static irqreturn_t key0_handler(int irq, void *filp)
{struct keyinput_dev *dev = filp;dev->timer.data = (volatile long)filp;mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20));return IRQ_HANDLED;
}

功能说明:

  1. 获取设备结构体指针
  2. 设置定时器数据
  3. 启动定时器(20ms后执行)
  4. 返回中断处理完成
8.4.2 定时器回调函数
static void timer_func(unsigned long arg)
{struct keyinput_dev *dev = (struct keyinput_dev *)arg;int value = 0;value = gpio_get_value(dev->key[0].gpio);if (value == 0){input_event(dev->inputdev, EV_KEY, KEY_0, 1);input_sync(dev->inputdev);}else{input_event(dev->inputdev, EV_KEY, KEY_0, 0);input_sync(dev->inputdev);}
}

功能说明:

  1. 获取设备结构体指针
  2. 读取GPIO引脚状态
  3. 根据状态报告按键事件
  4. 同步事件
8.4.3 定时器初始化函数
void timer1_init(struct keyinput_dev *dev)
{init_timer(&dev->timer);dev->timer.function = timer_func;
}

功能说明:

  1. 初始化定时器
  2. 设置定时器回调函数
8.4.4 按键初始化函数
int key_init(struct keyinput_dev *dev)
{u8 ret = 0, i = 0;dev->key_nd = of_find_node_by_path("/key");if (dev->key_nd == NULL){ret = -EFAULT;goto fail_find_nd;}dev->key[i].handler = key0_handler;dev->key[i].value = KEY0VALUE;for (i = 0; i < KEY_NUM; i++){dev->key[i].gpio = of_get_named_gpio(dev->key_nd, "key-gpios", i);if (dev->key[i].gpio < 0){ret = -EFAULT;goto fail_get_gpio;}memset(dev->key[i].name, 0, sizeof(dev->key[i].name));sprintf(dev->key[i].name, "KEY%d", i);ret = gpio_request(dev->key[i].gpio, dev->key[i].name);if (ret){ret = -EFAULT;goto fail_gpio_req;}ret = gpio_direction_input(dev->key[i].gpio);if (ret){ret = -EFAULT;goto fail_gpio_dir;}dev->key[i].irqnum = gpio_to_irq(dev->key[i].gpio);ret = request_irq(dev->key[i].irqnum, dev->key[i].handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, dev->key[i].name, &keyinputdev);if (ret){ret = -EFAULT;goto fail_req_irq;}}timer1_init(&keyinputdev);return 0;
fail_req_irq:printk("Kernel: fail_req_irq\r\n");
fail_gpio_dir:for (i = 0; i < KEY_NUM; i++){gpio_free(dev->key[i].gpio);}printk("Kernel: fail_gpio_dir\r\n");
fail_gpio_req:printk("Kernel: fail_gpio_req\r\n");
fail_get_gpio:printk("Kernel: fail_get_gpio\r\n");
fail_find_nd:printk("Kernel: fail_find_nd\r\n");return ret;
}

功能说明:

  1. 查找设备树节点
  2. 配置按键属性
  3. 获取GPIO编号
  4. 请求GPIO
  5. 设置GPIO方向为输入
  6. 获取中断号
  7. 请求中断
  8. 初始化定时器

错误处理采用goto模式,确保资源正确释放。

8.4.5 模块初始化函数
static int __init keyinput_init(void)
{int ret = 0;ret = key_init(&keyinputdev);if (ret < 0){goto fail_key_init;}keyinputdev.inputdev = input_allocate_device();if (keyinputdev.inputdev == NULL){ret = -EINVAL;goto fail_input_allocate;}keyinputdev.inputdev->name = KEYINPUT_NAME;__set_bit(EV_KEY, keyinputdev.inputdev->evbit);__set_bit(EV_REP, keyinputdev.inputdev->evbit);__set_bit(KEY_0, keyinputdev.inputdev->keybit);ret = input_register_device(keyinputdev.inputdev);if (ret){ret = -EINVAL;goto fail_input_reg;}return 0;
fail_input_reg:input_free_device(keyinputdev.inputdev);printk("Driver: fail_input_reg\r\n");
fail_input_allocate:printk("Driver: fail_input_allocate\r\n");
fail_key_init:printk("Driver: fail_key_init\r\n");return ret;
}

功能说明:

  1. 初始化按键
  2. 分配输入设备
  3. 设置设备属性
  4. 注册输入设备
8.4.6 模块退出函数
static void __exit keyinput_exit(void)
{u8 i = 0;input_unregister_device(keyinputdev.inputdev);input_free_device(keyinputdev.inputdev);for (i = 0; i < KEY_NUM; i++){free_irq(keyinputdev.key[i].irqnum, &keyinputdev);gpio_free(keyinputdev.key[i].gpio);}del_timer(&keyinputdev.timer);
}

功能说明:

  1. 注销输入设备
  2. 释放输入设备内存
  3. 释放中断
  4. 释放GPIO
  5. 删除定时器

8.5 模块声明

module_init(keyinput_init);
module_exit(keyinput_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("alientek");

9. 用户空间应用程序

9.1 应用程序功能

用户空间应用程序负责打开输入设备文件,读取按键事件,并显示按键状态。

9.2 代码分析

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <asm/ioctl.h>#define KEY0VALUE 0x10
#define KEYINVA 0xffint main(int argc, char *argv[])
{int cnt = 0;if (argc != 2){fprintf(stderr, "Usage: %s <led_device> <0|1>\n", argv[0]);return -1;}char *fileanme;unsigned char databuf[1];fileanme = argv[1];int fd = 0;fd = open(fileanme, O_RDWR);if (fd < 0){perror("open led device error");return -1;}while (1){unsigned char ch = 0;int ret = read(fd, &ch, sizeof(ch));// printf("User: ret is: %d", ret);if (ret < 0){// printf("User: read error");}else{if (ch == KEY0VALUE){printf("User: key is pressing, ret is: %d\r\n", ret);}}// sleep(100);}printf("APP runing finished! \r\n");close(fd);return 0;
}

9.3 功能说明

  1. 参数检查: 检查命令行参数数量
  2. 设备打开: 打开输入设备文件
  3. 事件读取: 循环读取按键事件
  4. 事件处理: 判断按键状态并输出信息
  5. 资源释放: 关闭设备文件

9.4 改进建议

当前应用程序存在以下问题:

  1. 错误的设备打开模式: 输入设备通常以只读模式打开
  2. 错误的读取方式: 输入事件应该使用struct input_event结构体读取
  3. 缺少事件解析: 没有正确解析输入事件

改进版本:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>int main(int argc, char *argv[])
{if (argc != 2) {fprintf(stderr, "Usage: %s <input_device>\n", argv[0]);return -1;}int fd = open(argv[1], O_RDONLY);if (fd < 0) {perror("open input device");return -1;}struct input_event ev;while (1) {ssize_t n = read(fd, &ev, sizeof(ev));if (n == (ssize_t)-1) {perror("read");break;} else if (n != sizeof(ev)) {fprintf(stderr, "short read\n");break;}if (ev.type == EV_KEY && ev.code == KEY_0) {if (ev.value == 1) {printf("Key 0 pressed\n");} else if (ev.value == 0) {printf("Key 0 released\n");}}}close(fd);return 0;
}

10. 编译与部署

10.1 Makefile分析

KERNERDIR := /home/ubuntu2004/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENTDIR := $(shell pwd)obj-m := keyinput.o
build : kernel_moduleskernel_modules:$(MAKE) -C $(KERNERDIR) M=$(CURRENTDIR) modulesclean:$(MAKE) -C $(KERNERDIR) M=$(CURRENTDIR) clean
10.1.1 变量说明
  • KERNERDIR: 内核源码目录
  • CURRENTDIR: 当前目录
  • obj-m: 要编译的模块对象
10.1.2 目标说明
  • build: 编译目标
  • kernel_modules: 实际编译命令
  • clean: 清理目标

10.2 编译步骤

  1. 设置环境变量

    export ARCH=arm
    export CROSS_COMPILE=arm-linux-gnueabihf-
    
  2. 编译模块

    make -C /path/to/kernel M=$(pwd) modules
    
  3. 清理

    make -C /path/to/kernel M=$(pwd) clean
    

10.3 部署步骤

  1. 复制模块到目标板

    scp keyinput.ko root@target:/lib/modules/$(uname -r)/
    
  2. 加载模块

    insmod keyinput.ko
    
  3. 验证模块加载

    lsmod | grep keyinput
    
  4. 检查设备节点

    ls /dev/input/
    

14. 扩展应用

14.1 多按键支持

修改驱动以支持多个按键:

#define KEY_NUM 4struct key_desc key[KEY_NUM] = {{.name = "KEY0", .gpio = -1, .irqnum = -1, .value = KEY_0},{.name = "KEY1", .gpio = -1, .irqnum = -1, .value = KEY_1},{.name = "KEY2", .gpio = -1, .irqnum = -1, .value = KEY_2},{.name = "KEY3", .gpio = -1, .irqnum = -1, .value = KEY_3},
};

14.2 自定义事件处理

添加自定义事件处理逻辑:

static void custom_event_handler(struct keyinput_dev *dev, int key_value)
{switch(key_value) {case KEY_0:// 处理KEY0事件break;case KEY_1:// 处理KEY1事件break;default:break;}
}

14.3 与其他子系统集成

将输入事件与其他子系统集成:

// 与LED子系统集成
extern void led_control(int state);static void timer_func(unsigned long arg)
{struct keyinput_dev *dev = (struct keyinput_dev *)arg;int value = gpio_get_value(dev->key[0].gpio);if (value == 0) {input_event(dev->inputdev, EV_KEY, KEY_0, 1);led_control(1); // 点亮LED} else {input_event(dev->inputdev, EV_KEY, KEY_0, 0);led_control(0); // 熄灭LED}input_sync(dev->inputdev);
}

参考文档

  • Linux内核源码: https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
  • 设备树规范: https://www.devicetree.org/
  • 《Linux设备驱动程序》第三版
  • 《深入理解Linux内核》第三版

https://gitee.com/dream-cometrue/linux_driver_imx6ull

http://www.dtcms.com/a/359262.html

相关文章:

  • [光学原理与应用-332]:ZEMAX - 序列模式与非序列模式的本质、比较
  • FPGA 实现FOC 无刷电机控制器
  • 电子健康记录风险评分与多基因风险评分的互补性与跨系统推广性研究
  • 洛谷 P1395 会议 -普及/提高-
  • 吴恩达机器学习(四)
  • 10. 函数和匿名函数(二)
  • 深入理解 shared_ptr 与 weak_ptr:访问控制与线程安全
  • 广东省省考备考(第九十天8.30)——判断推理(第十节课)
  • Java多线程初阶
  • C++讲解---如何设计一个类
  • 防火墙技术(三):状态检测和会话机制
  • 接口自动化测试框架
  • python pyqt5开发DoIP上位机【自动化测试的逻辑是怎么实现的?】
  • 深度解析Fluss LockUtils类的并发艺术
  • 手写MyBatis第43弹:插件拦截原理与四大可拦截对象详解
  • Agent实战教程:LangGraph结构化输出详解,让智能体返回格式化数据
  • Keil5 MDK_541官网最新版下载、安装
  • offsetof宏的实现
  • 线程池项目代码细节2
  • 互联网医院系统源码解析:如何从零搭建高效的在线问诊平台
  • SNMPv3开发--EngineID安全访问机制
  • 腾讯云的运维笔记——从yum的安装与更新源开始
  • 深入理解 Linux 驱动中的 file_operations:从 C 语言函数指针到类比 C++ 虚函数表
  • centos7中MySQL 5.7.32 到 5.7.44 升级指南:基于官方二进制包的原地替换式升级
  • 有个需求:切换车队身份实现Fragment的Tab隐藏显示(车队不显示奖赏)
  • SNMPv3开发--简单使用
  • 【Linux基础】深入理解Linux环境下的BIOS机制
  • Python - 机器学习:从 “教电脑认东西” 到 “让机器自己学规律”
  • 项目管理和产品管理的区别
  • docker,mysql安装