利用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中的代码

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;

		// 每次循环都重新初始化flags
    	flags = 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中的代码

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,代码如下:

c 复制代码
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)线程中:

c 复制代码
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了,代码如下:

c 复制代码
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() 创建一个专用的工作队列。每个工作队列会有独立的线程,互不干扰。

    • 示例代码:

      c 复制代码
      #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,可以并发运行多个任务。
      • 内核会动态分配线程来处理这些任务,从而减少任务阻塞的影响。
    • 示例代码:

      c 复制代码
      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文件内容

bash 复制代码
# 使用不同的Linux内核时, 一定要修改KERN_DIR,KERN_DIR代表已经配置、编译好的Linux源码的根目录

KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88

all:
	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` clean
	rm -rf modules.order
	rm -f button_test_02

obj-m += gpio_key_drv.o

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

源码复制到Ubuntu中。

bash 复制代码
make

将交叉编译出的gpio_key_drv.kobutton_test_02复制到NFS文件目录中,备用。

加载模块

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

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

检查设备文件生成没有

bash 复制代码
ls /dev/

有了:

运行测试程序

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

c 复制代码
echo "7 4 1 7" > /proc/sys/kernel/printk

然后:

bash 复制代码
cd /mnt/workqueue
bash 复制代码
./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 线程:

      bash 复制代码
      ps -e | grep kworker
  4. 调试信息中的 kworker

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

总结

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

卸载驱动程序模块

c 复制代码
rmmod gpio_key_drv.ko

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

附完整工程文件

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