Linux驱动开发进阶(六)- 多线程与并发
文章目录
- 1、前言
- 2、进程与线程
- 3、内核线程
- 4、底半步机制
- 4.1、软中断
- 4.2、tasklet
- 4.3、工作队列
- 4.3.1、普通工作项
- 4.3.2、延时工作项
- 4.3.3、工作队列
- 5、中断线程化
- 6、进程
- 6.1、内核进程
- 6.2、用户空间进程
- 7、锁机制
- 7.1、原子操作
- 7.2、自旋锁
- 7.3、信号量
- 7.4、互斥锁
- 7.5、completion
1、前言
- 学习参考书籍以及本文涉及的示例程序:李山文的《Linux驱动开发进阶》
- 本文属于个人学习后的总结,不太具备教学功能。
2、进程与线程
略。
3、内核线程
在linux中,线程和进程实际上是同一个东西,本质就是为了完成任务。因此,linus将这个成为task,即任务。在内核中使用struct task_struct表示,包含了进程的各种信息,如进程ID、父进程指针、进程状态、进程优先级、进程的内存管理信息等。
4、底半步机制
linux内核中,对于硬件中断的处理,将中断服务函数拆分为两个部分,其中需要紧急处理的放在上半部分,主要处理一些与硬件以及关键数据结构相关的事情。将不那么紧急的事情放在下半部分。我们将上半部分称之为顶半部,将下半部分称之为底半部。
4.1、软中断
软中断一般很少直接用于实现下半部。软中断就是软件实现的异步中断,它的优先级比硬中断低,但比普通进程优先级高,同时它和硬中断一样不能休眠。Linux内核中的软中断数组如下所示,用来记录软中断的向量(软中断服务函数):
enum
{HI_SOFTIRQ=0,TIMER_SOFTIRQ,NET_TX_SOFTIRQ,NET_RX_SOFTIRQ,BLOCK_SOFTIRQ,IRQ_POLL_SOFTIRQ,TASKLET_SOFTIRQ,SCHED_SOFTIRQ,HRTIMER_SOFTIRQ,RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */NR_SOFTIRQS
};
4.2、tasklet
tasklet依赖于软中断,内核使用一个链表的方式来管理所有的tasklet任务。tasklet的使用如下:先定义一个struct tasklet_struct结构体,然后使用tasklet_setup函数初始化(可能再比较老的内核版本是用tasklet_init()来初始化),最后使用tasklet_schedule函数来调度。
下面程序举例如何使用tasklet。在按键中断中发起tasklet调用。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/sysfs.h>
#include <linux/gpio/consumer.h>
#include <linux/gpio.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/wait.h>
#include <linux/poll.h>#define PIN_NUM 117 // gpio3_PC5struct gpio_key {dev_t dev_num;struct cdev cdev;struct class *class;struct device *dev;struct tasklet_struct tasklet;
};static struct gpio_key *key;static irqreturn_t key_irq(int irq, void *args)
{tasklet_schedule(&key->tasklet);return IRQ_HANDLED;
}static int key_open (struct inode * inode, struct file * file)
{return 0;
}static int key_close(struct inode * inode, struct file * file)
{return 0;
}static struct file_operations key_ops = {.owner = THIS_MODULE,.open = key_open,.release = key_close,
};static void tasklet_handler(unsigned long data)
{printk(KERN_INFO "tasklet demo!\n");
}static int __init async_init(void)
{int ret, irq;key = kzalloc(sizeof(struct gpio_key), GFP_KERNEL);if(key == NULL) {printk(KERN_ERR "struct gpio_key alloc failed\n");return -ENOMEM;;}tasklet_init(&key->tasklet, tasklet_handler, 0);if (!gpio_is_valid(PIN_NUM)) {kfree(key);printk(KERN_ERR "gpio is invalid\n");return -EPROBE_DEFER;}ret = gpio_request(PIN_NUM, "key");if(ret) {kfree(key);printk(KERN_ERR "gpio request failed\n");return ret;}irq = gpio_to_irq(PIN_NUM);if (irq < 0) {printk(KERN_ERR "get gpio irq failed\n");goto err;}ret = request_irq(irq, key_irq, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | IRQF_ONESHOT, "key", key);if(ret) {printk(KERN_ERR "request irq failed\n");goto err;}ret = alloc_chrdev_region(&key->dev_num ,0, 1, "key"); //动态申请一个设备号if(ret !=0) {unregister_chrdev_region(key->dev_num, 1);printk(KERN_ERR "alloc_chrdev_region failed!\n");return -1;}key->cdev.owner = THIS_MODULE;cdev_init(&key->cdev, &key_ops);cdev_add(&key->cdev, key->dev_num, 1);key->class = class_create(THIS_MODULE, "key_class");if(key->class == NULL) {printk(KERN_ERR "key_class failed!\n");goto err1;}key->dev = device_create(key->class, NULL, key->dev_num, NULL, "key");if(IS_ERR(key->dev)) {printk(KERN_ERR "device_create failed!\n");goto err2;}return ret;
err2:class_destroy(key->class);
err1:unregister_chrdev_region(key->dev_num, 1);
err:gpio_free(PIN_NUM);kfree(key);return -1;
}static void __exit async_exit(void)
{//停止tasklet任务tasklet_disable(&key->tasklet);// 清理tasklet相关资源tasklet_kill(&key->tasklet);gpio_free(PIN_NUM);device_destroy(key->class, key->dev_num);class_destroy(key->class);unregister_chrdev_region(key->dev_num, 1);free_irq(gpio_to_irq(PIN_NUM), key);kfree(key);
}module_init(async_init);
module_exit(async_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("1477153217@qq.com");
MODULE_DESCRIPTION("async notify test");

4.3、工作队列
实际tasklet还是适合处理较快的任务,因为tasklet不可被抢占,同时tasklet无法让任务在多个核心上执行。
4.3.1、普通工作项
下面示例程序展示了如何使用工作队列。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/delay.h>
#include <linux/workqueue.h>
#include <linux/kthread.h>//定义一个任务
struct task_struct *thread_worker;
//定义一个工作项
struct work_struct work;void work_func(struct work_struct *work)
{printk(KERN_INFO "work execute!\n");
}static int test_thread(void *data)
{while(!kthread_should_stop()) {schedule_work(&work);msleep(1000);}return 0;
}static int __init work_init(void)
{INIT_WORK(&work, work_func);//创建一个线程thread_worker = kthread_run(test_thread, NULL, "test_kthread");if (IS_ERR(thread_worker)) {return PTR_ERR(thread_worker);}return 0;
}static void __exit work_exit(void)
{kthread_stop(thread_worker);cancel_work_sync(&work);
}module_init(work_init);
module_exit(work_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("1477153217@qq.com");
MODULE_VERSION("0.1");
MODULE_DESCRIPTION("work demo");

4.3.2、延时工作项
即延时一段时间再执行。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/delay.h>
#include <linux/workqueue.h>
#include <linux/kthread.h>//定义一个任务
struct task_struct *thread_worker;
//定义一个工作项
struct work_struct work;void work_func(struct work_struct *work)
{printk(KERN_INFO "work execute!\n");
}static int test_thread(void *data)
{while(!kthread_should_stop()) {schedule_work(&work);msleep(1000);}return 0;
}static int __init work_init(void)
{INIT_WORK(&work, work_func);//创建一个线程thread_worker = kthread_run(test_thread, NULL, "test_kthread");if (IS_ERR(thread_worker)) {return PTR_ERR(thread_worker);}return 0;
}static void __exit work_exit(void)
{kthread_stop(thread_worker);cancel_work_sync(&work);
}module_init(work_init);
module_exit(work_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("1477153217@qq.com");
MODULE_VERSION("0.1");
MODULE_DESCRIPTION("work demo");

4.3.3、工作队列
当有多个工作项时,可以放到工作队列里。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/delay.h>
#include <linux/workqueue.h>
#include <linux/kthread.h>//定义一个任务队列指针
static struct workqueue_struct *workqueue = NULL;
//定义一个任务
struct task_struct *thread_worker = NULL;
//定义一个工作项1
struct work_struct work1;
//定义一个工作项2
struct work_struct work2;void work1_func(struct work_struct *work)
{printk(KERN_INFO "work1 execute!\n");
}void work2_func(struct work_struct *work)
{printk(KERN_INFO "work2 execute!\n");
}static int test_thread(void *data)
{while(!kthread_should_stop()) {//将work1放到工作队列中执行queue_work(workqueue,&work1);//将work2放到工作队列中执行queue_work(workqueue,&work2);msleep(1000);}return 0;
}static int __init work_init(void)
{INIT_WORK(&work1, work1_func);INIT_WORK(&work2, work2_func);workqueue = create_singlethread_workqueue("wq_test");if(workqueue == NULL){return -1;}//创建一个线程thread_worker = kthread_run(test_thread, NULL, "test_kthread");if (IS_ERR(thread_worker)) {destroy_workqueue(workqueue);return PTR_ERR(thread_worker);}return 0;
}static void __exit work_exit(void)
{kthread_stop(thread_worker);destroy_workqueue(workqueue);cancel_work_sync(&work1);cancel_work_sync(&work2);
}module_init(work_init);
module_exit(work_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("1477153217@qq.com");
MODULE_VERSION("0.1");
MODULE_DESCRIPTION("work queue demo");

5、中断线程化
上面我们介绍了底半部的一些机制,有软中断、tasklet、工作队列。但为了进一步提高系统实时性,又将顶半步进一步拆分为硬件中断处理和线程化中断。(下图来自李山文的《Linux驱动开发进阶》)

硬件中断处理:仅执行最紧急的任务(如读取硬件寄存器、应答中断)。仍然在中断上下文中执行(不可睡眠,快速完成)。
线程化处理:剩余的顶半部逻辑移至一个专用的内核线程中执行。在进程上下文中运行(可睡眠,可被高优先级任务抢占)。
申请一个线程化中断使用如下函数:
int request_threaded_irq(unsigned int irq, irq_handler_t handler,irq_handler_t thread_fn, unsigned long irqflags,const char *devname, void *dev_id)
下面是一个示例程序:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/sysfs.h>
#include <linux/gpio/consumer.h>
#include <linux/gpio.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/wait.h>#define PIN_NUM 117 static int ev_press=0;static irqreturn_t key_irq(int irq, void *args)
{return IRQ_WAKE_THREAD;
}static irqreturn_t key_irq_thread(int irq, void *args)
{ev_press = 1; //按下按键printk(KERN_INFO "key press!\n");return IRQ_HANDLED;
}static int __init thread_irq_init(void)
{int ret, irq;if (!gpio_is_valid(PIN_NUM)) {printk(KERN_ERR "gpio is invalid\n");return -EPROBE_DEFER;}ret = gpio_request(PIN_NUM, "key");if(ret) {printk(KERN_ERR "gpio request failed\n");return -1;}irq = gpio_to_irq(PIN_NUM);if (irq < 0) {gpio_free(PIN_NUM);printk(KERN_ERR "get gpio irq failed\n");return -1;}ret = request_threaded_irq(irq, key_irq, key_irq_thread, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | IRQF_ONESHOT, "key", &ev_press);if(ret) {gpio_free(PIN_NUM);printk(KERN_ERR "request irq failed\n");return -1;}return 0;
}static void __exit thread_irq_exit(void)
{gpio_free(PIN_NUM);free_irq(gpio_to_irq(PIN_NUM), &ev_press);
}module_init(thread_irq_init);
module_exit(thread_irq_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("1477153217@qq.com");
MODULE_DESCRIPTION("thread irq test");

6、进程
6.1、内核进程
引用书本原话:“Linux内核将所有的线程都当作进程来实现,每个线程都有一个唯一的task_struct(进程控制块),在内核中看起来就像一个普通的进程,只是它与其他进程共享一些资源,如地址空间。所以从内核的角度来看,进程和线程没有本质区别,只是在资源共享上有所不同。用户空间可以使用clone、fork、vfork系统调用来创建进程,其最终调用的都是内核中的_do_fork函数。_do_fork函数调用copy_process函数来创建子进程的task_struct数据结构。”
(下图来自李山文的《Linux驱动开发进阶》)

6.2、用户空间进程
在应用程序创建进程有如下函数:fork、vfork、clone、pthread_create。
在内核(kernel)层面,最终都会被表示为一个 task_struct 数据结构。
7、锁机制
7.1、原子操作
在操作系统中一句C语言代码是非常有可能被打断的,为了防止这种情况发生,我们需要使用原子操作。



7.2、自旋锁
自旋锁就是不停的判断一个锁变量是否可用,如果可用,则继续执行,否则一直等待。因此,自旋锁适合用在一些任务频繁调度的时候。自旋锁还有一个特点是不可能引起睡眠,因此在中断上下文中,必须使用自旋锁来实现临界区的访问。
初始化一个自旋锁:
spinlock_t lock;
spin_lock_init(&lock);
获取锁和释放锁:
spin_lock(&lock);
spin_unlock(&lock);
但使用自旋锁时,如果产生了中断,在中断服务程序中也尝试获取锁,那么就会产生死锁,对于这种情况,应该先关闭中断再获取锁,相关操作函数如下:


7.3、信号量
信号量是一个整型变量。P 操作用于申请资源,如果资源不可用(信号量 ≤ 0),则进程阻塞,直到资源可用。V 操作用于释放资源,并唤醒等待的进程(如果有)。信号量是一种会导致进程睡眠的锁机制,对于需要等待很长时间的进程而言,就需要采用信号量。
初始化一个信号量:
struct semaphore semap;
sema_init(&semap, 5);
PV操作相关的函数如下:

7.4、互斥锁
互斥锁在linux内核中使用较多,大部分情况下,都是对全局变量做保护。
初始化一个互斥锁:
struct mutex tlb_lock;
mutex_init(&tlb_lock);
对互斥锁上锁和解锁:

7.5、completion
当我们需要初始化一些东西,但在另一个线程必须等待这些初始化完成后才能继续执行。为此linux提供了completion机制。
动态定义一个完成量:
struct completion setup_done;
init_completion(&setup_done);
在需要等待的地方调用wait_for_completion即可:
complete(&setup_done);
complete_all(&setup_done);
相关文章:
Linux驱动开发进阶(六)- 多线程与并发
文章目录 1、前言2、进程与线程3、内核线程4、底半步机制4.1、软中断4.2、tasklet4.3、工作队列4.3.1、普通工作项4.3.2、延时工作项4.3.3、工作队列 5、中断线程化6、进程6.1、内核进程6.2、用户空间进程 7、锁机制7.1、原子操作7.2、自旋锁7.3、信号量7.4、互斥锁7.5、comple…...
买不起了,iPhone 或涨价 40% ?
周知的原因,新关税对 iPhone 的打击,可以说非常严重。 根据 Rosenblatt Securities分析师的预测,若苹果完全把成本转移给消费者。 iPhone 16 标配版的价格,可能上涨43%。 iPhone 16 标配的价格是799美元,上涨43%&am…...
Axure 列表滚动:表头非常多(横向滚动方向)、分页(纵向滚动) | 基于动态面板的滚动方向和取消调整大小以适合内容两个属性进行实现
文章目录 引言I 列表滚动的操作说明see also共享原型引言 Axure RP9教程 【数据传输】(页面值传递)| 作用域 :全局变量、局部变量 https://blog.csdn.net/z929118967/article/details/147019839?spm=1001.2014.3001.5501 基于动态面板的滚动方向和取消调整大小以适合内容两…...
RBAC 权限控制:深入到按钮级别的实现
RBAC 权限控制:深入到按钮级别的实现 一、前端核心思路 1. 大致实现思路 后端都过SELECT连表查询把当前登录的用户对应所有的权限返回过来,前端把用户对应所有的权限 存起来to(vuex/pinia) 中 ,接着前端工程师需要知道每个按钮对应的权限代…...
大模型格式化输出的几种方法
大模型格式化输出的几种方法 在开发一些和LLM相关的应用的时候,如何从大模型的反馈中拿到结构化的输出数据是非常重要的,那么本文就记录几种常用的方法。 OpenAI提供的新方法 在 OpenAI 的 Python 库中,client.beta.chat.completions.parse 是一个用于生成结构化输出的方法…...
【区间贪心】合并区间 / 无重叠区间 / 用最少数量的箭引爆气球 / 俄罗斯套娃信封问题
⭐️个人主页:小羊 ⭐️所属专栏:贪心算法 很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~ 目录 合并区间无重叠区间用最少数量的箭引爆气球俄罗斯套娃信封问题 合并区间 合并区间 class Solution { public:vector<vecto…...
JBDC java数据库连接(2)
目录 JBDC建立 获得PrepareStatement执行sql语句 形式: PrepareStatement中的方法: 实例 PreparedStatement和Statement 基于以下的原因: JBDC建立 获得PrepareStatement执行sql语句 在sql语句中参数位置使用占位符,使用setXX方法向sql中设置参数 形式&…...
es --- 集群数据迁移
目录 1、需求2、工具elasticdump2.1 mac安装问题解决 2.2 elasticdump文档 3、迁移 1、需求 迁移部分新集群没有的索引和数据 2、工具elasticdump Elasticdump 的工作原理是将输入发送到输出 。两者都可以是 elasticsearch URL 或 File 2.1 mac安装 前置:已经安装…...
Redis高频面试题及深度解析(20大核心问题+场景化答案)
摘要:Redis作为高性能缓存与内存数据库,是后端开发的核心技术栈之一。本文整理20大高频Redis面试题,结合真实场景与底层源码逻辑,助你彻底掌握Redis核心机制。涵盖单线程模型、集群方案、分布式锁、持久化等核心知识点。 一、Redi…...
事件处理程序
事件处理程序 一、事件处理程序的定义 事件处理程序是一段代码,用于响应特定的事件。在网页开发中,事件是在文档或浏览器窗口中发生的特定交互瞬间,如用户点击按钮、页面加载完成等。事件处理程序则是针对这些事件执行的函数,它能…...
stable diffusion部署ubuntu
stable-diffusion webui: https://github.com/AUTOMATIC1111/stable-diffusion-webui python3.10 -m venv venv(3.11的下torch会慢得要死) source venv/bin/activate 下载checkpoint模型放入clip_version"/home/chen/软件/stable-diffusion-webu…...
Qt的window注册表读写以及删除
Qt的window注册表读写以及删除 1. 使用 QSettings(Qt推荐方式)基本操作关键点限制 2. 调用Windows原生API示例:创建/读取键值常用API注意事项 3. 高级场景(1) 递归删除键(2) 注册表权限修改 4. 安全性建议总结其他QT文章推荐 在Qt中操作Windo…...
聊一聊接口测试时遇到上下游依赖时该如何测试
目录 一、手工测试时的处理方法 1.1沟通协调法 1.2模拟数据法 二、自动化测试时的处理方法 2.1 数据关联法(变量提取) 2.2 Mock数据法 2.3自动化框架中的依赖管理 三、实施示例(以订单接口测试为例) 3.1Mock依赖接口&…...
C++ 排序(1)
以下是一些插入排序的代码 1.插入排序 1.直接插入排序 // 升序 // 最坏:O(N^2) 逆序 // 最好:O(N) 顺序有序 void InsertSort(vector<int>& a, int n) {for (int i 1; i < n; i){int end i - 1;int tmp a[i];// 将tmp插入到[0,en…...
【有啥问啥】深入浅出讲解 Teacher Forcing 技术
深入浅出讲解 Teacher Forcing 技术 在序列生成任务(例如机器翻译、文本摘要、图像字幕生成等)中,循环神经网络(RNN)以及基于 Transformer 的模型通常采用自回归(autoregressive)的方式生成输出…...
zk基础—zk实现分布式功能
1.zk实现数据发布订阅 (1)发布订阅系统一般有推模式和拉模式 推模式:服务端主动将更新的数据发送给所有订阅的客户端。 拉模式:客户端主动发起请求来获取最新数据(定时轮询拉取)。 (2)zk采用了推拉相结合来实现发布订阅 首先客户端需要向服务端注册自己关…...
mySQL数据库和mongodb数据库的详细对比
以下是 MySQL 和 MongoDB 的详细对比,涵盖优缺点及适用场景: 一、核心特性对比 特性MySQL(关系型数据库)MongoDB(文档型 NoSQL 数据库)数据模型结构化表格,严格遵循 Schema灵活的文档模型&…...
ubuntu wifi配置(命令行版本)
1、查询当前设备环境的wifi列表 nmcli dev wifi list2、连接wifi nmcli dev wifi connect "MiFi-SSID" password "Password" #其中MiFi-SSID是wifi的密码,Password是wifi的密码3、查看连接情况 nmcli dev status...
Docker与Kubernetes在ZKmall开源商城容器化部署中的应用
ZKmall开源商城作为高并发电商系统,其容器化部署基于DockerKubernetes技术栈,实现了从开发到生产环境的全流程标准化与自动化。以下是核心应用场景与技术实现: 一、容器化基础:Docker镜像与微服务隔离 服务镜像标准化 分层构建…...
华为AI-agent新作:使用自然语言生成工作流
论文标题 WorkTeam: Constructing Workflows from Natural Language with Multi-Agents 论文地址 https://arxiv.org/pdf/2503.22473 作者背景 华为,北京大学 动机 当下AI-agent产品百花齐放,尽管有ReAct、MCP等框架帮助大模型调用工具࿰…...
MYSQL数据库语法补充
一,DQL基础查询 DQL(Data Query Language)数据查询语言,可以单表查询,也可以多表查询 语法: select 查询结果 from 表名 where 条件; 特点: 查询结果可以是:表中的字段…...
Elasticsearch单节点安装手册
Elasticsearch单节点安装手册 以下是一份 Elasticsearch 单节点搭建手册,适用于 Linux 系统(如 CentOS/Ubuntu),供学习和测试环境使用。 Elasticsearch 单节点搭建手册 1. 系统要求 操作系统:Linux(Cent…...
在Windows搭建gRPC C++开发环境
一、环境构建 1. CMake Download CMake 2. Git Git for Windows 3. gRPC源码 git clone -b v1.48.0 https://github.com/grpc/grpc 进入源码目录 cd grpc 下载依赖库 git submodule update --init 二、使用CMake生成工程文件 三、使用vs2019编译grpc库文件 四、使用…...
[Python] 企业内部应用接入钉钉登录,端内免登录+浏览器授权登录
[Python] 为企业网站应用接入钉钉鉴权,实现钉钉客户端内自动免登授权,浏览器中手动钉钉授权登录两种逻辑。 操作步骤 企业内部获得 开发者权限,没有的话先申请。 访问 钉钉开放平台-应用开发 创建一个 企业内部应用-钉钉应用。 打开应用…...
编程题学习
acwing 826. 单链表 #include <iostream>using namespace std;const int N 100010;int idx, e[N], ne[N], head;void init() {head -1;idx 0; }void insert_head(int x) {e[idx] x;ne[idx] head;head idx ; }void delete_k_pos(int x, int k) {e[idx] x;ne[idx…...
Dev C++单个源文件和项目两种编程方式介绍
Dev C单个源文件和项目两种编程方式介绍 Dev-C 是一款免费、开源的 C/C 集成开发环境(IDE),专为初学者和中级程序员设计,具有简单易用、功能丰富等特点。 Dev C 支持单文件编程和项目编程两种方式。它们之间的主要区别在于如何组…...
用AbortController取消事件绑定
视频教程 React - 🤔 Abort Controller 到底是什么神仙玩意?看完这个视频你就明白了!💡_哔哩哔哩_bilibili AbortController的好处之一是事件绑定的函数已无需具名函数,匿名函数也可以被取消事件绑定了 //该代码2秒后点击失效…...
解决:Fontconfig head is null, check your fonts or fonts configurat
文章目录 问题解决方案安装字体依赖包强制刷新字体缓存验证是否生效 个人简介 问题 在使用 Java 环境部署或运行图形相关应用时,比如图片验证码,偶尔会遇到如下报错: Fontconfig head is null, check your fonts or fonts configurat意味当…...
this指针 和 类的继承
一、this指针 Human类的属性fishc与Human()构造器的参数fishc同名,但却是两个东西。使用this指针让构造器知道哪个是参数,哪个是属性。 this指针:指向当前的类生成的对象 this -> fishc fishc当前对象(…...
无锡无人机驾驶证培训费用
无锡无人机驾驶证培训费用,随着科技的迅速发展,无人机在众多行业中发挥着举足轻重的作用。从影视制作到农业监测,再到物流运输与城市规划,无人机的应用场景不断扩展,因此越来越多的人开始意识到学习无人机驾驶技能的重…...
