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

利用Linux的工作队列(Workqueue)实现中断下半部的处理

本文代码在哪个基础上修改而成?

本文是在博文 https://blog.csdn.net/wenhao_ir/article/details/145228617 的代码基础上修改而成。

关于工作队列(Workqueue)的概念

工作队列(Workqueue)可以用于实现Linux的中断下半部的,之前在博文 https://blog.csdn.net/wenhao_ir/article/details/145309140 中已经介绍过中断上半部和中断下半部的概念。

它和软中断(SoftIRQ)、任务队列(Tasklet)相比,最大的不同是它是可以进入阻塞或休眠状态,它允许调用会导致阻塞或休眠的函数,比如msleepmutex_lockschedule 等函数。
当然,在三者中,工作队列(Workqueue)的优先级相对来说是最低的。

本文利用工作队列(Workqueue)实现中断下半部的思路是:在硬中断中将任务加入到处理工作队列(Workqueue)的内核线程中,然后由这个内核线程去调度这个任务的执行。

完整源代码

驱动程序gpio_key_drv.c中的代码

#include <linux/module.h>#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/workqueue.h>
#include <asm/current.h>
#include <linux/delay.h>struct gpio_key{int gpio;struct gpio_desc *gpiod;int flag;int irq;struct work_struct work;
} ;static struct gpio_key *gpio_keys_100ask;/* 主设备号                                                                 */
static int major = 0;
static struct class *gpio_key_class;static int g_key = 0;static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);/* 环形缓冲区 */
#define BUF_LEN 128
static int g_keys[BUF_LEN];
static int r, w;#define NEXT_POS(x) ((x+1) % BUF_LEN)static int is_key_buf_empty(void)
{return (r == w);
}static int is_key_buf_full(void)
{return (r == NEXT_POS(w));
}static void put_key(int key_value)
{if (!is_key_buf_full()){g_keys[w] = key_value;w = NEXT_POS(w);}
}static int get_key(void)
{int key_value = 0;if (!is_key_buf_empty()){key_value = g_keys[r];r = NEXT_POS(r);}return key_value;
}/* 实现文件操作结构体中的read函数  */
static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{//printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);int err;int key_value;wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());//从缓形缓冲区中取出数据key_value = get_key();err = copy_to_user(buf, &key_value, 4);// 返回值为4表明读到了4字节的数据return 4;
}/* 定义自己的file_operations结构体                                              */
static struct file_operations gpio_key_drv = {.owner	 = THIS_MODULE,.read    = gpio_key_drv_read,
};static void key_work_func(struct work_struct *work)
{struct gpio_key *gpio_key = container_of(work, struct gpio_key, work);int val;val = gpiod_get_value(gpio_key->gpiod);printk("The function keyw_ork_func is sleeping for 1000 milliseconds...\n");// 内核空间函数msleep可使线程休眠一段时间,单位为毫秒// 需要包含头文件 #include <linux/delay.h>// 在中断下半部中,只有工作队列(Workqueue)才能进行休眠操作msleep(1000);// g_key的高8位中存储的是GPIO口的编号,低8位中存储的是按键按下时的逻辑值g_key = (gpio_key->gpio << 8) | val;//装按键值放入环形缓冲区put_key(g_key);wake_up_interruptible(&gpio_key_wait);printk("key_work_func: the process is %s pid %d\n",current->comm, current->pid); // current->comm代表当前进程(线程)的名字	printk("key_work_func key %d %d\n", gpio_key->gpio, val);
}static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{struct gpio_key *gpio_key = dev_id;// 任务加入到内核kworker线程的工作队列上schedule_work(&gpio_key->work);return IRQ_HANDLED;  // 表示中断已处理
}/* 1. 从platform_device获得GPIO* 2. gpio=>irq* 3. request_irq*/
static int gpio_key_probe(struct platform_device *pdev)
{int err;// 获取设备树节点指针struct device_node *node = pdev->dev.of_node;// count用于存储设备树中描述的GPIO口的数量int count;int i;enum of_gpio_flags flag;unsigned flags = GPIOF_IN;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);count = of_gpio_count(node);if (!count){printk("%s %s line %d, there isn't any gpio available\n", __FILE__, __FUNCTION__, __LINE__);return -1;}gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);if (!gpio_keys_100ask) {printk("Memory allocation failed for gpio_keys_100ask\n");return -ENOMEM;}for (i = 0; i < count; i++){//  获取GIPO的全局编号及其标志位信息的代码gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);if (gpio_keys_100ask[i].gpio < 0){printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);return -1;}// 获取GPIO口的GPIO描述符的代码gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);if (!gpio_keys_100ask[i].gpiod) {printk("Failed to get GPIO descriptor for GPIO %d\n", gpio_keys_100ask[i].gpio);return -EINVAL;}// 结构体gpio_key的成员flag用于存储对应的GPIO口是否是低电平有效,假如是低电平有效,成员flag的值为1,假如不是低电平有效,成员flag的值为0。// 后续代码实际上并没有用到成员flag,这里出现这句代码只是考虑到代码的可扩展性,所以在这里是可以删除的。gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;// 每次循环都重新初始化flagsflags = GPIOF_IN;// 假如GPIO口是低电平有效,则把flags添加上低电平有效的信息if (flag & OF_GPIO_ACTIVE_LOW)flags |= GPIOF_ACTIVE_LOW;// 请求一个GPIO硬件资源与设备结构体`pdev->dev`进行绑定// 注意,这个绑定操作会在调用函数platform_driver_unregister()注销platform_driver时自动由内核解除绑定操作,所以gpio_key_remove函数中不需要显示去解除绑定// 由`devm`开头的函数通常都会内核自动管理资源,咱们在退出函数中不用人为的去释放资源或解除绑定。err = devm_gpio_request_one(&pdev->dev, gpio_keys_100ask[i].gpio, flags, NULL);// 获取GPIO口的中断请求号gpio_keys_100ask[i].irq  = gpio_to_irq(gpio_keys_100ask[i].gpio);// 初始化工作队列(Workqueue)INIT_WORK(&gpio_keys_100ask[i].work, key_work_func);}for (i = 0; i < count; i++){char irq_name[32];  // 用于存储动态生成的中断名称//使用snprintf()函数将动态生成的中断名称写入irq_name数组snprintf(irq_name, sizeof(irq_name), "swh_gpio_irq_%d", i);  // 根据i生成名称//调用函数request_irq()来请求并设置一个中断err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_FALLING, irq_name, &gpio_keys_100ask[i]);}/* 注册file_operations 	*/major = register_chrdev(0, "swh_read_keys_major", &gpio_key_drv);  gpio_key_class = class_create(THIS_MODULE, "swh_read_keys_class");if (IS_ERR(gpio_key_class)) {printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);unregister_chrdev(major, "swh_read_keys_major");return PTR_ERR(gpio_key_class);}// 由于这里是把多个按键看成是一个设备,你可以想像一个键盘上对应多个按键,但键盘本身是一个设备,所以只有一个设备文件device_create(gpio_key_class, NULL, MKDEV(major, 0), NULL, "read_keys0"); /* /dev/read_keys0 */return 0;}static int gpio_key_remove(struct platform_device *pdev)
{struct device_node *node = pdev->dev.of_node;int count;int i;device_destroy(gpio_key_class, MKDEV(major, 0));class_destroy(gpio_key_class);unregister_chrdev(major, "swh_read_keys_major");count = of_gpio_count(node);for (i = 0; i < count; i++) {// 只有在irq有效时才释放中断资源if (gpio_keys_100ask[i].irq >= 0) {// 释放GPIO中断资源,下面这句代码做了下面两件事:// 1、解除 `gpio_keys_100ask[i].irq` 中断号和 `gpio_key_isr` 中断处理函数的绑定。// 2、解除 `gpio_keys_100ask[i].irq` 中断号和中断处理函数与 `gpio_keys_100ask[i]` 数据结构的绑定。free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);}// 释放GPIO描述符if (gpio_keys_100ask[i].gpiod) {gpiod_put(gpio_keys_100ask[i].gpiod);}}// 释放内存kfree(gpio_keys_100ask);return 0;
}static const struct of_device_id irq_matach_table[] = {{ .compatible = "swh-gpio_irq_key" },{ },
};/* 1. 定义platform_driver */
static struct platform_driver gpio_keys_driver = {.probe      = gpio_key_probe,.remove     = gpio_key_remove,.driver     = {.name   = "swh_irq_platform_dirver",.of_match_table = irq_matach_table,},
};/* 2. 在入口函数注册platform_driver */
static int __init gpio_key_init(void)
{int err;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);err = platform_driver_register(&gpio_keys_driver); return err;
}/* 3. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数*     卸载platform_driver*/
static void __exit gpio_key_exit(void)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);platform_driver_unregister(&gpio_keys_driver);
}/* 7. 其他完善:提供设备信息,自动创建设备节点                                     */module_init(gpio_key_init);
module_exit(gpio_key_exit);MODULE_LICENSE("GPL");

测试程序button_test.c中的代码

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <time.h>/** ./button_test /dev/100ask_button0**/// 打印线程的执行函数
void* print_while_waiting(void* arg)
{while (1){printf("I am another thread, and while the main thread is waiting for a button to be pressed, I can still run normally.\n");sleep(10); // 每隔10秒打印一次}return NULL;
}int main(int argc, char **argv)
{int fd;int val;pthread_t print_thread;int keystroke = 0; //记录按键次数/* 1. 判断参数 */if (argc != 2) {printf("Usage: %s <dev>\n", argv[0]);return -1;}/* 2. 打开文件 */fd = open(argv[1], O_RDWR);if (fd == -1){printf("Can not open file %s\n", argv[1]);return -1;}// 创建一个线程,每隔一段时间打印输出一条信息表示在等待按键期间,另外的线程在继续正常执行。if (pthread_create(&print_thread, NULL, print_while_waiting, NULL) != 0){printf("Failed to create print thread\n");close(fd);return -1;}while (1){/* 3. 读文件 */read(fd, &val, 4);/* 提取 GPIO 编号和逻辑值 */int gpio_number = (val >> 8) & 0xFF; // 高8位为 GPIO 编号int gpio_value = val & 0xFF;         // 低8位为逻辑值keystroke++;/* 打印读到的信息 */printf("GPIO Number: %d, Logical Value: %d\n", gpio_number, gpio_value);printf("keystrokes is %d\n", keystroke);}//pthread_join的作用是使主线程等待线程print_threa结束后再继续执行剩下的代码。//如果主线程在结束时未等待子线程完成,可能会导致未完成的资源清理或意外的程序终止。//这里由于主线程中有个条件永远为真的while循环,实际上这句代码没有实际作用。pthread_join(print_thread, NULL);close(fd);return 0;
}

与工作队列(Workqueue)相关的代码解读

由于工作队列(Workqueue)还是属于中断下半部一种,所以和前面的内核定时器(https://blog.csdn.net/wenhao_ir/article/details/145281064) 和 任务队列(Tasklet)(https://blog.csdn.net/wenhao_ir/article/details/145309140) 的使用基本相同。

首先还是为按键结构体 struct gpio_key添加一个类型为work_struct的成员。
在这里插入图片描述
为什么呢?因为每一个GPIO口我们都要为其分配一个work_struct结构体。

然后在platform中的probe操作函数gpio_key_probe对每个GPIO口初始化时,为每个GPIO口初始化一个work,代码如下:
在这里插入图片描述

INIT_WORK(&gpio_keys_100ask[i].work, key_work_func);

第1个参数就是每个按键对应的work_struct结构体的实例,第2个参数是任务的回调函数。
肯定要问:怎么不为回调函数传入数据?答:因为数据存储于gpio_keys_100ask[i]中,我们可以通过&gpio_keys_100ask[i].work反推出gpio_keys_100ask[i]的指针位置,所以其实数据是已经传入了。这一点和新版的内核定时器是一样的,详情见 https://blog.csdn.net/wenhao_ir/article/details/145281064 【搜索“注意-Linux_5.x以上对内核定时器进行了修改”】

然后我们在硬中断处理函数中把任务加入到系统内核的工作队列(Workqueue)线程中:

static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{struct gpio_key *gpio_key = dev_id;// 任务加入到内核kworker线程的工作队列上schedule_work(&gpio_key->work);return IRQ_HANDLED;  // 表示中断已处理
}

代码很简单,详细的描述略。
注意: schedule_work函数并不一定要运行于硬中断的处理函数中,具体的情况本文后面有说明。

接下来就去看任务的回调函数key_work_func了,代码如下:

static void key_work_func(struct work_struct *work)
{struct gpio_key *gpio_key = container_of(work, struct gpio_key, work);int val;val = gpiod_get_value(gpio_key->gpiod);printk("The function keyw_ork_func is sleeping for 1000 milliseconds...\n");// 内核空间函数msleep可使线程休眠一段时间,单位为毫秒// 需要包含头文件 #include <linux/delay.h>// 在中断下半部中,只有工作队列(Workqueue)才能进行休眠操作msleep(1000);// g_key的高8位中存储的是GPIO口的编号,低8位中存储的是按键按下时的逻辑值g_key = (gpio_key->gpio << 8) | val;//装按键值放入环形缓冲区put_key(g_key);wake_up_interruptible(&gpio_key_wait);printk("key_work_func: the process is %s pid %d\n",current->comm, current->pid); // current->comm代表当前进程(线程)的名字	printk("key_work_func key %d %d\n", gpio_key->gpio, val);
}

同样属于中断下半部,工作队列(Workqueue)和软中断(SoftIRQ)、任务队列(Tasklet)相比,最大的不同是它是可以进入阻塞或休眠状态,它允许调用会导致阻塞或休眠的函数,比如msleepmutex_lockschedule 等函数,所以我们这里就利用内核心空间函数msleep使其休眠1000毫秒再运行,从而看是不是真的可以进行休眠状态。

代码很简单,没啥好说的,只是代码末尾处还利用printk打印出了内核处理工作队列的线程的名字和进程号,具体关于名字的分析在测试程序之后我有分析。

在本代码中的工作队列(Workqueue)不需要释放什么资源,因为其占用的资源由内核处理工作队列(Workqueue)的线程和相关机制自动管理, 所以不需要释放什么资源,所以函数gpio_key_remove中不需要增添对工作队列(Workqueue)相关资源的释放。

至此,与工作队列(Workqueue)相关的代码分析完毕。

工作队列(Workqueue)机制的缺点及解决方法

工作队列(Workqueue)的缺点:
前面的队列任务阻塞时会影响后面的队列任务的执行,因为它们相当于是挂在同一个内核心线程上的任务。

详细解释如下:

schedule_work() 函数会将工作项(work_struct 类型)添加到内核中的一个 工作队列 上,而这个工作队列会由内核管理的一个专用线程(或多个线程)来执行。


详细原理

  1. 工作队列的核心

    • 工作队列(Workqueue)是 Linux 内核提供的一种机制,用于在进程上下文中延迟执行任务。
    • schedule_work() 是将工作项加入到 system_wq(内核默认的全局工作队列)中。
  2. 内核线程执行

    • 内核为工作队列创建了一个专用的内核线程(kworker),通常命名为 kworker/<CPU编号>
    • 当你调用 schedule_work() 时,内核将你的工作项添加到队列中,kworker 线程会取出并执行这些工作项。
  3. 为什么要这样设计?

    • 中断上下文无法进行阻塞或复杂操作,但很多任务需要在进程上下文中运行。
    • 工作队列为这种需求提供了解决方案:允许开发者在内核线程中运行需要延迟执行的任务,同时允许这些任务休眠、阻塞或执行耗时操作。

schedule_work() 的执行过程

  1. 调用 schedule_work() 时:

    • 检查 work_struct 是否已在队列中(防止重复排队)。
    • 如果没有重复排队,将它添加到全局工作队列(system_wq)。
  2. 内核线程(kworker)被唤醒:

    • kworker 线程会检查它负责的队列是否有任务。
    • 如果有任务,取出并调用工作项的处理函数(由开发者定义)。
  3. 运行你的工作项:

    • 调用你在 INIT_WORK() 中指定的处理函数。
    • 工作项处理完成后,从队列中移除。

在队列任务的执行中,如果前面的队列任务进入了阻塞状态,就会影响后续队列任务的执行。这是因为 工作队列 中的任务是按照 FIFO(先进先出)顺序依次执行的,而一个任务阻塞后,kworker 线程会一直等待任务完成,无法继续处理后续任务。


工作队列的运行机制

  1. 默认情况:一个 kworker 线程

    • 系统默认的全局工作队列(system_wq)使用共享的 kworker 线程。
    • kworker 线程是单线程处理的,一次只能运行一个任务。如果某个任务阻塞,后续任务必须等待。
  2. 顺序处理的特点

    • 如果工作队列中前面的任务 A 阻塞了,kworker 会等待任务 A 完成,再去处理任务 B。
    • 任务之间没有抢占关系,因此阻塞会直接导致后续任务延迟执行。

如何避免阻塞影响其他任务?
有几种方法可以解决这个问题:

  1. 使用线程化的中断处理
    如果是中断处理中使用工作队列(Workqueue)实现中断下半部的处理,那么可以在注册中断的同时为这个中断注册一个属于这个中断的线程。详情见 https://blog.csdn.net/wenhao_ir/article/details/145326705

  2. 创建独立的工作队列

    • 使用 alloc_workqueue() 创建一个专用的工作队列。每个工作队列会有独立的线程,互不干扰。
    • 示例代码:
      #include <linux/workqueue.h>static struct workqueue_struct *my_wq;
      static struct work_struct my_work;static void my_work_handler(struct work_struct *work)
      {msleep(5000); // 模拟阻塞操作printk(KERN_INFO "Task finished\n");
      }static int __init my_init(void)
      {// 创建独立的工作队列my_wq = alloc_workqueue("my_workqueue", WQ_UNBOUND, 0);if (!my_wq)return -ENOMEM;// 初始化并提交工作项到自定义工作队列INIT_WORK(&my_work, my_work_handler);queue_work(my_wq, &my_work);printk(KERN_INFO "Work queued\n");return 0;
      }static void __exit my_exit(void)
      {// 销毁工作队列if (my_wq)destroy_workqueue(my_wq);printk(KERN_INFO "Module exited\n");
      }module_init(my_init);
      module_exit(my_exit);
      MODULE_LICENSE("GPL");
      
    • 通过 alloc_workqueue() 创建的工作队列有独立的内核线程,任务阻塞不会影响其他队列的任务。

  1. 使用 WQ_UNBOUND 属性
    • 在创建工作队列时使用 WQ_UNBOUND 标志:
      • 允许工作项不绑定到特定的 CPU,可以并发运行多个任务。
      • 内核会动态分配线程来处理这些任务,从而减少任务阻塞的影响。
    • 示例代码:
      alloc_workqueue("my_workqueue", WQ_UNBOUND, 0);
      

  1. 避免任务长时间阻塞
  • 如果任务本身需要长时间阻塞,可以考虑拆分任务,将长时间的阻塞部分移到用户态完成,或者异步处理(如通过线程或其他机制)。

总结

  • 默认行为:全局工作队列(system_wq)使用共享的 kworker 线程,阻塞任务会影响后续任务的执行。
  • 解决办法
    1. 创建专用的工作队列(独立线程)。
    2. 使用 WQ_UNBOUND 来增加并发能力。
    3. 避免任务本身长时间阻塞。

通过这些方法,可以有效避免阻塞任务影响整个工作队列的运行效率。

schedule_work函数并不一定要运行在硬件中断的处理函数中

schedule_work 函数并不一定要运行在硬件中断的处理函数中。它可以在任何可以运行内核代码的上下文中被调用,具体包括以下场景:


  1. 硬件中断处理函数中调用
  • 硬件中断的处理函数通常要求执行迅速,因此适合将复杂或耗时的任务推迟到中断下半部(如工作队列)中执行。
  • 在中断处理函数中调用 schedule_work,将工作任务添加到工作队列中,由内核的工作线程(kworker)在合适的时机处理。

  1. 内核线程或其他上下文中调用
    • schedule_work 可以在任何普通的内核上下文中调用,比如:
      • 从设备驱动的 probe 或其他文件操作函数中。
      • 在定时器回调函数中。
      • 在内核模块的入口初始化函数(module_init)中。
    • 无需限制在中断上下文中使用。

  1. 用户态系统调用触发的内核函数中
  • 用户态程序触发的系统调用(例如读写驱动设备)中,驱动程序可以调用 schedule_work 将任务推迟到工作队列中执行。
  • 这可以避免耗时任务阻塞用户态进程。

为什么不一定要在中断上下文中调用?

  • 设计目的: schedule_work 的作用是将任务加入到工作队列,它本身的调用非常轻量,可以在任何允许调用内核函数的上下文中使用。
  • 上下文限制:
    • 如果是中断上下文,不能执行可能会阻塞的操作(如 msleepmutex_lock)。通过 schedule_work,可以将任务推迟到工作线程(kworker)中运行,从而避开这些限制。
    • 如果是进程上下文,则不存在中断上下文的限制,可以直接使用。

注意事项

  • 调用 schedule_work 后,必须确保对应的 work_struct 已正确初始化(通过 INIT_WORKINIT_DELAYED_WORK)。
  • 如果模块卸载时还有未完成的工作队列任务,需要确保处理完毕或取消任务,避免资源泄漏或非法访问。

总结
schedule_work 并不一定需要在硬件中断处理函数中调用。它可以在任何允许执行内核代码的上下文中调用,目的是将任务加入工作队列,交由 kworker 内核线程在合适的时间处理。这样做可以延迟执行复杂任务,减少上下文的阻塞时间,提高内核代码的效率和响应性。

设备树文件的修改和更新

和下面两篇博文一样:
https://blog.csdn.net/wenhao_ir/article/details/145225508
https://blog.csdn.net/wenhao_ir/article/details/145176361

Makfile文件内容

# 使用不同的Linux内核时, 一定要修改KERN_DIR,KERN_DIR代表已经配置、编译好的Linux源码的根目录KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88all:make -C $(KERN_DIR) M=`pwd` modules# 因为测试程序中有线程的创建,所以下面的语句需要添加 -lpthread 链接选项$(CROSS_COMPILE)gcc -o button_test_02 button_test.c -lpthread clean:make -C $(KERN_DIR) M=`pwd` cleanrm -rf modules.orderrm -f button_test_02obj-m += gpio_key_drv.o

交叉编译出驱动模块和测试程序

源码复制到Ubuntu中。
在这里插入图片描述

make

在这里插入图片描述
将交叉编译出的gpio_key_drv.kobutton_test_02复制到NFS文件目录中,备用。

在这里插入图片描述

加载模块

打开串口终端→打开开发板→挂载网络文件系统

mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
insmod /mnt/workqueue/gpio_key_drv.ko

在这里插入图片描述

检查设备文件生成没有

ls /dev/

有了:
在这里插入图片描述

运行测试程序

先把内核printk打印的显示打开:

echo "7 4 1 7" > /proc/sys/kernel/printk

然后:

cd /mnt/workqueue
./button_test_02 /dev/read_keys0

在这里插入图片描述
从运行过程可以感知到,工作队列所在的内核线程的确是休眠1000毫秒之后再继续运行的。在延迟1000毫秒之后后面的代码把按键值放入了环形缓冲区,进面用户空间中的程序可以读取相应的按键值。
测试成功。

关于处理工作队列的内核线程的名字的详细解释

我们的程序在测试中还打印出了处理工作队列的内核线程的名字和进程号,名字为kworker/0:1,其所在进程号为1720,我们可以用ps命令看一下:
在这里插入图片描述
可见,内核中确实存在着一个进程号为1720的内核线程在处理中断下半部的工作队列(Workqueue)。

这里我解释一下名字的kworker/0:1含义:

进程名字 kworker/0:1 是 Linux 内核中 kworker(内核工作线程) 的标准命名格式。我们来逐个解析其含义:


  1. kworker
    • 表示这是一个 内核工作线程(Kernel Worker Thread)。
    • 这些线程是由内核的 Workqueue(工作队列) 机制管理的,用于处理一些延迟执行的任务或繁重的内核工作。

  1. 0
  • 表示 CPU 的编号
  • 这里的 0 指的是第 0 号 CPU,也就是说,这个 kworker 线程被绑定或调度在 CPU 0 上运行。

  1. 1
    • 表示 线程的 ID,也可以理解为线程在某个 CPU 上的序号。
    • 每个 CPU 可能会有多个 kworker 线程,它们会被分配一个唯一的 ID 来区分。在本例中,1 表示第一个线程。

完整解释

  • kworker/0:1 表示:
    • 这是一个内核工作线程。
    • 它被绑定(或主要调度)在 CPU 0 上运行。
    • 它是 CPU 0 上的第 1 号 kworker 线程

补充说明

  1. 多核系统中的 kworker

    • 在多核系统中,每个 CPU 都可能有多个 kworker 线程。例如:
      • kworker/1:0:表示 CPU 1 上的第 0 号 kworker 线程。
      • kworker/2:2:表示 CPU 2 上的第 2 号 kworker 线程。
  2. 动态生成的 kworker 线程

    • kworker 线程的数量和命名不是固定的,它们根据内核的工作负载动态生成和销毁。
    • 如果内核的某些任务需要延迟执行或者负载增加时,会创建更多的 kworker 线程。
  3. 如何查看这些线程

    • 使用 htoptop 或直接在 /proc 文件系统中可以看到这些 kworker 线程。
    • 比如运行以下命令可以列出所有 kworker 线程:
      ps -e | grep kworker
      
  4. 调试信息中的 kworker

    • dmesg 或内核日志中经常会看到 kworker 线程参与的内核任务,它们通常用来执行 硬件中断处理的延迟部分文件系统同步网络包处理 等。

总结

  • kworker/0:1 是内核的工作线程。
  • 0 是 CPU 编号,表示线程绑定在 CPU 0 上。
  • 1 是线程序号,表示 CPU 0 上的第 1 号工作线程。

卸载驱动程序模块

rmmod gpio_key_drv.ko

在这里插入图片描述
运行上面命令后,过了较长时间系统仍然能正常运行,说明卸载没有问题。证明说明工作队列(Workqueue)的相关资源确实并不需要释放。原因就是其占用的资源由内核处理工作队列(Workqueue)的线程和相关机制自动管理, 所以不需要释放什么资源。

附完整工程文件

https://pan.baidu.com/s/1b6Nysvb4zU9B1bNQNeh3rw?pwd=cvjq

相关文章:

利用Linux的工作队列(Workqueue)实现中断下半部的处理

本文代码在哪个基础上修改而成&#xff1f; 本文是在博文 https://blog.csdn.net/wenhao_ir/article/details/145228617 的代码基础上修改而成。 关于工作队列(Workqueue)的概念 工作队列(Workqueue)可以用于实现Linux的中断下半部的&#xff0c;之前在博文 https://blog.cs…...

LabVIEW处理复杂系统和数据处理

LabVIEW 是一个图形化编程平台&#xff0c;广泛应用于自动化控制、数据采集、信号处理、仪器控制等复杂系统的开发。它的图形化界面使得开发人员能够直观地设计系统和算法&#xff0c;尤其适合处理需要实时数据分析、高精度控制和复杂硬件集成的应用场景。LabVIEW 提供丰富的库…...

spring-springboot -springcloud

目录 spring: 动态代理: spring的生命周期(bean的生命周期): SpringMvc的生命周期: SpringBoot: 自动装配: 自动装配流程: Spring中常用的注解&#xff1a; Spring Boot中常用的注解&#xff1a; SpringCloud: 1. 注册中心: 2. gateway(网关): 3. Ribbon(负载均…...

DRG/DIP 2.0时代下基于PostgreSQL的成本管理实践与探索(下)

五、数据处理与 ETL 流程编程实现 5.1 数据抽取与转换(ETL) 在 DRG/DIP 2.0 时代的医院成本管理中,数据抽取与转换(ETL)是将医院各个业务系统中的原始数据转化为可供成本管理分析使用的关键环节。这一过程涉及从医院 HIS 系统中抽取患者诊疗数据,并对其进行格式转换、字…...

打造本地音乐库

文章目录 存储介质硬盘&#xff08;NAS&#xff09;媒体播放器&#xff08;可视MP3、MP4&#xff09;实体介质&#xff08;CD光盘、黑胶片&#xff09;注意事项为什么不使用在线音乐&#xff08;App&#xff09;和网盘打造一套HiFi系统的成本非常高 获取音乐正版音乐途径免费音…...

【2024 - 年终总结】叶子增长,期待花开

写在前面&#xff1a;本博客仅作记录学习之用&#xff0c;部分图片来自网络&#xff0c;如需引用请注明出处&#xff0c;同时如有侵犯您的权益&#xff0c;请联系删除&#xff01; 文章目录 前言论博客创作保持2024的记录清单博客科研开源工作生活 总结与展望互动致谢参考 前言…...

python 统计相同像素值个数

目录 python 统计相同像素值个数 最大值附近的值 python 统计相同像素值个数 import cv2 import numpy as np import time from collections import Counter# 读取图像 image cv2.imread(mask16.jpg)# 将图像转换为灰度图像 gray_image cv2.cvtColor(image, cv2.COLOR_BGR2…...

蓝卓“1+2+N”智慧工厂架构,让工业智能更简单

面对复杂的工业环境、海量的数据以及多样化的业务需求&#xff0c;如何实现智能化转型&#xff0c;让工业智能触手可及&#xff0c;成为了众多企业面临的难题。蓝卓以创新精神为引领&#xff0c;推出了“12N”智慧工厂架构&#xff0c;旨在简化工业智能的实现路径&#xff0c;让…...

12、MySQL锁相关知识

目录 1、全局锁和表锁使用场景 2、行锁的意义 3、为什么说间隙锁解决了快照的幻读? 4、RR隔离级别产生幻读的场景 5、详解元数据锁(MDL)作用以及如何减少元数据锁 6、出现死锁场景 7、查看MySQL锁情况 8、自增锁 1、全局锁和表锁使用场景 全局锁 备份数据库:当需要…...

某大厂一面:HashMap 的put方法具体做了哪些操作

HashMap 的 put 方法是一个常用的操作&#xff0c;它将一个键值对插入到哈希表中。下面是 put 方法执行的详细流程&#xff0c;包括各个步骤的解释&#xff0c;并附上相应的代码片段。 1. 检查键是否为 null 如果传入的键为 null&#xff0c;HashMap 会特别处理这种情况&…...

WPF基础 | 深入 WPF 事件机制:路由事件与自定义事件处理

WPF基础 | 深入 WPF 事件机制&#xff1a;路由事件与自定义事件处理 一、前言二、WPF 事件基础概念2.1 事件的定义与本质2.2 常见的 WPF 事件类型 三、路由事件3.1 路由事件的概念与原理3.2 路由事件的三个阶段3.3 路由事件的标识与注册3.4 常见的路由事件示例 四、自定义事件处…...

精选100+套HTML可视化大屏模板源码素材

大屏数据可视化以大屏为主要展示载体的数据可视化设计。 “大面积、炫酷动效、丰富色彩”&#xff0c;大屏易在观感上给人留下震撼印象&#xff0c;便于营造某些独特氛围、打造仪式感。 原本看不见的数据可视化后&#xff0c;便能调动人的情绪、引发人的共鸣。 使用方法&…...

如何使用Python爬虫按关键字搜索AliExpress商品:代码示例与实践指南

在电商领域&#xff0c;能够按关键字搜索并获取商品信息对于市场分析、选品和竞品研究至关重要。AliExpress&#xff08;速卖通&#xff09;作为全球知名的跨境电商平台&#xff0c;提供了丰富的商品数据。以下将详细介绍如何使用Python爬虫按关键字搜索AliExpress商品&#xf…...

No.36 学习 | Python 函数:从基础到实战

最近我在学 Python 编程&#xff0c;今天可算是狠狠钻研了一把 Python 里的函数&#xff0c;感觉脑袋里的知识又充实了不少&#xff0c;赶紧来记一记。 一、Python函数基础概念 &#xff08;一&#xff09;pass语句&#xff1a;代码块的“占位符” 在编写代码时&#xff0c;有…...

Unity常用特性(Attribute)用法

一.UnityEngine命名空间 1.[Header(string)] inspector面板上给显示的字段上加一个描述 通常情况下&#xff0c;用于在 Inspector 窗口中创建字段的逻辑分组 public class AttributeTest : MonoBehaviour {[Header("public_field_num")]public int num; }2.[Tool…...

VUE对接deepseekAPI调用

1.先去开放平台注册账号申请api key。开放平台&#xff1a;https://platform.deepseek.com/api_keys 2.你的项目需要有发送请求的axios或者自己写。 npm install axios # 或 yarn add axios 3.创建 API 调用函数 在 Vue 项目中&#xff0c;通常会将 API 调用的逻辑封装到一个…...

【Postman 接口测试】接口测试基础知识

在软件开发与测试领域&#xff0c;接口测试是保障软件质量的关键环节之一&#xff0c;而 Postman 作为一款功能强大且广泛使用的接口测试工具&#xff0c;能帮助我们高效地进行接口测试工作。下面&#xff0c;我们将详细介绍接口测试的基础知识&#xff0c;包括接口的认识、接口…...

谷粒商城——商品服务-三级分类

1.商品服务-三级分类 1.1三级分类介绍 1.2查询三级分类查询-递归树型结构数据获取 1.2.1导入数据pms_catelog.sql到数据表pms_category 1.2.2一次性查出所有分类及子分类 1.2.2.1修改CategoryController.java /*** 查出所有分类以及子分类&#xff0c;以树形结构组装起来*/R…...

视觉语言模型 (VLMs):跨模态智能的探索

文章目录 一. VLMs 的重要性与挑战&#xff1a;连接视觉与语言的桥梁 &#x1f309;二. VLMs 的核心训练范式&#xff1a;四种主流策略 &#x1f5fa;️1. 对比训练 (Contrastive Training)&#xff1a;拉近正例&#xff0c;推远负例 ⚖️2. 掩码方法 (Masking)&#xff1a;重构…...

HarmonyOS NEXT:华为分享-碰一碰开发分享

随着科技的不断进步&#xff0c;智能手机和智能设备之间的互联互通变得越来越重要。华为作为科技行业的领军企业&#xff0c;一直致力于为用户提供更加便捷、高效的使用体验。HarmonyOS NEXT系统的推出&#xff0c;特别是其中的“碰一碰”功能&#xff0c;为用户带来了前所未有…...

宝塔Linux+docker部署nginx出现403 Forbidden

本文主要讲述了宝塔docker部署nginx出现403 Forbidden的原因&#xff0c;以及成功部署前端的方法步骤。 目录 1、问题描述2、问题检测2.1 检测监听端口是否异常2.2 检测Docker容器是否异常2.2.1 打开宝塔Linux的软件商店&#xff0c;找到Docker管理器&#xff0c;查看前端容器是…...

软件测试丨Redis 的数据同步策略以及数据一致性保证

Redis 以其键值存储的方式&#xff0c;为开发者提供了数据快速存取的能力。它不仅支持丰富的数据结构&#xff0c;如字符串、哈希、列表、集合等&#xff0c;而且提供了高效的数据同步与一致性保障机制。正因为如此&#xff0c;Redis 被广泛应用于缓存、消息队列、实时数据分析…...

C语言-运算符

1. 按位与运算符&#xff08;&&#xff09; 按位与运算符对两个整数的每一位执行“与”操作。只有当两个相应位都为 1 时&#xff0c;结果才为 1 &#xff1b;否则为 0。 // 示例 int a 5; // 二进制: 0101 int b 3; // 二进制: 0011 int result a & b; …...

困境如雾路难寻,心若清明步自轻---2024年创作回顾

文章目录 前言博客创作回顾第一次被催更第一次获得证书周榜几篇博客互动最多的最满意的引发思考的 写博契机 碎碎念时也运也部分经验 尾 前言 今年三月份&#xff0c;我已写下一篇《近一年多个人总结》&#xff0c;当时还没开始写博客。四月份写博后&#xff0c;就顺手将那篇总…...

表格标签基本使用

表格主要用于显示、展示数据&#xff0c;因为它可以让数据显示的非常的规整&#xff0c;可读性非常好。特别是后台展示数据的时候&#xff0c;能够熟练运用表格就显得很重要。一个清爽简约的表格能够把繁杂的教据表现得很有条理。 1.<table></table>是用于定义表格…...

【学术会议论文投稿】深度解码:机器学习与深度学习的界限与交融

目录 一、定义与起源&#xff1a;历史长河中的两条轨迹 二、原理差异&#xff1a;从浅层到深层的跨越 三、代码解析&#xff1a;实战中的机器学习与深度学习 机器学习示例&#xff1a;线性回归 深度学习示例&#xff1a;卷积神经网络(CNN) 四、应用差异&#xff1a;各自领…...

使用printmap()函数来打印地图

使用PrintMap()函数可以将地图布局发送到打印机.默认情况下,任务会发送到地图文档保存的默认打印机,但也可以通过自定义一个特定的打印机来执行打印任务 操作方法 1.打开目标地图 2.打开python窗口 3.导入arcpy.mapping模块 import arcpy.mapping as mapping 4.引用活动地…...

MyBatis Plus 的 InnerInterceptor:更轻量级的 SQL 拦截器

在 Spring Boot 项目中使用 MyBatis Plus 时&#xff0c;你可能会遇到 InnerInterceptor 这个概念。 InnerInterceptor 是 MyBatis Plus 提供的一种轻量级 SQL 拦截器&#xff0c;它与传统的 MyBatis 拦截器&#xff08;Interceptor&#xff09;有所不同&#xff0c;具有更简单…...

Java复习第四天

一、代码题 1.相同的树 (1)题目 给你两棵二叉树的根节点p和q&#xff0c;编写一个函数来检验这两棵树是否相同。 如果两个树在结构上相同&#xff0c;并且节点具有相同的值&#xff0c;则认为它们是相同的。 示例 1: 输入:p[1,2,3]&#xff0c;q[1,2,3] 输出:true示例 2: 输…...

docker 安装 mysql 详解

在平常的开发工作中&#xff0c;我们经常需要用到 mysql 数据库。那么在docker容器中&#xff0c;应该怎么安装mysql数据库呢。简单来说&#xff0c;第一步&#xff1a;拉取镜像&#xff1b;第二步&#xff1a;创建挂载目录并设置 my.conf&#xff1b;第三步&#xff1a;启动容…...