0
点赞
收藏
分享

微信扫一扫

Linux学习笔记(17.9)—— 在工作队列处理按键后续工作

Hyggelook 2022-03-12 阅读 33
  1. 工作队列

    ​ 软中断运行在中断上下文中,因此不能阻塞和睡眠,而tasklet使用软中断实现,当然也不能阻塞和睡眠。但如果某延迟处理函数需要睡眠或者阻塞呢?没关系工作队列就可以如您所愿了。
    ​ 把推后执行的任务叫做工作(work),描述它的数据结构为work_struct ,这些工作以队列结构组织成工作队列(workqueue),其数据结构为workqueue_struct,而工作线程就是负责执行工作队列中的工作。系统默认的工作者线程为events
    ​ 工作队列(work queue)是另外一种将工作推后执行的形式。工作队列可以把工作推后,交由一个内核线程去执行—这个下半部分总是会在进程上下文执行,但由于是内核线程,其不能访问用户空间。最重要特点的就是工作队列允许重新调度甚至是睡眠。
    ​ 通常,在工作队列和软中断/tasklet中作出选择非常容易。可使用以下规则:

    • 如果推后执行的任务需要睡眠,那么只能选择工作队列。
    • 如果推后执行的任务需要延时指定的时间再触发,那么使用工作队列,因为其可以利用timer延时(内核定时器实现)。
    • 如果推后执行的任务需要在一个tick之内处理,则使用软中断或tasklet,因为其可以抢占普通进程和内核线程,同时不可睡眠。
    • 如果推后执行的任务对延迟的时间没有任何要求,则使用工作队列,此时通常为无关紧要的任务。
      实际上,工作队列的本质就是将工作交给内核线程处理,因此其可以用内核线程替换。但是内核线程的创建和销毁对编程者的要求较高,而工作队列实现了内核线程的封装,不易出错,所以我们也推荐使用工作队列。
  2. 工作队列相关数据结构

    ​ 实际上,工作队列的本质就是将工作交给内核线程处理,因此其可以用内核线程替换。但是内核线程的创建和销毁对编程者的要求较高,而工作队列实现了内核线程的封装,不易出错,所以我们也推荐使用工作队列。

    • 工作结构体

      struct work_struct {
          atomic_long_t data; 		/* 传递给工作函数的参数 */
      #define WORK_STRUCT_PENDING 0   /* T if work item pending execution */
      #define WORK_STRUCT_FLAG_MASK (3UL)
      #define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)
          struct list_head entry; 	/* 链表结构,链接同一工作队列上的工作。 */
          work_func_t func; 			/* 工作函数,用户自定义实现 */
      #ifdef CONFIG_LOCKDEP
          struct lockdep_map lockdep_map;
      #endif
      };
      
      /* 工作队列执行函数的原型: 
       * 该函数会由一个工作者线程执行,因此其在进程上下文中,可以睡眠也可以中断。但只能在内核中运行,
       * 无法访问用户空间
       */
      void (*work_func_t) (struct work_struct *work);
      
    • 延迟工作结构体(延迟的实现是在调度时延迟插入相应的工作队列)

      struct delayed_work {
          struct work_struct work;
          struct timer_list timer; /* 定时器,用于实现延迟处理 */
      };
      
    • 工作队列结构体

      struct workqueue_struct {
          struct cpu_workqueue_struct *cpu_wq; /* 指针数组,其每个元素为per-cpu的工作队列 */
          struct list_head list;
          const char *name;
          int singlethread; 	/* 标记是否只创建一个工作者线程 */
          int freezeable;     /* Freeze threads during suspend */
          int rt;
      #ifdef CONFIG_LOCKDEP
          struct lockdep_map lockdep_map;
      #endif
      };
      
    • cpu核工作队列(每个cpu核都对应一个工作者线程worker_thread)

       struct cpu_workqueue_struct {
          spinlock_t lock;
          struct list_head worklist;
          wait_queue_head_t more_work;
          struct work_struct *current_work;
          struct workqueue_struct *wq;
          struct task_struct *thread;
      } cacheline_aligned;
      
    • 缺省工作队列API

      // 静态创建 
      DECLARE_WORK(name,function); 		/* 定义正常执行的工作项 */
      DECLARE_DELAYED_WORK(name,function);/* 定义延后执行的工作项 */
      
      // 动态创建
      INIT_WORK(_work, _func) 			/* 创建正常执行的工作项 */
      INIT_DELAYED_WORK(_work, _func)		/* 创建延后执行的工作项 */
      
      // 调度默认工作队列
      int schedule_work(struct work_struct *work)
      
      /* 对正常执行的工作进行调度,即把给定工作的处理函数提交给缺省的工作队列和工作者线程。工作者线程本质上
      * 是一个普通的内核线程,在默认情况下,每个CPU核均有一个类型为“events”的工作者线程,当调用schedule_work
      * 时,这个工作者线程会被唤醒去执行工作链表上的所有工作。
      * 系统默认的工作队列名称是:keventd_wq, 默认的工作者线程叫:events/n,这里的n是处理器的编号,每个处
      * 理器对应一个线程。比如,单处理器的系统只有events/0这样一个线程。而双处理器的系统就会多一个events/1
      * 线程。
      * 默认的工作队列和工作者线程由内核初始化时创建:
      */
      start_kernel()-->rest_init-->do_basic_setup-->init_workqueues
      
      // 调度延迟工作
      int schedule_delayed_work(struct delayed_work *dwork,unsigned long delay)
      
      // 刷新缺省工作队列
      void flush_scheduled_work(void)     /* 此函数会一直等待,直到队列中的所有工作都被执行 */
      
      /* 取消延迟工作 */
      static inline int cancel_delayed_work(struct delayed_work *work)
      // flush_scheduled_work并不取消任何延迟执行的工作,因此,如果要取消延迟工作,应该调用cancel_delayed_work。
      

      ​ 以上均是采用内核线程来实现工作队列,其优点是简单易用,缺点是如果缺省工作队列负载太重,执行效率会很低,这就需要我们创建自己的工作者线程和工作队列。

    • 自定义工作队列

      // 宏定义 返回值为工作队列,name为工作线程名称。创建新的工作队列和相应的工作者线程,name用于该内核线程的命名。
      create_workqueue(name) 
      
      // 类似于schedule_work,区别在于queue_work把给定工作提交给创建的工作队列wq而不是缺省队列。
      int queue_work(struct workqueue_struct *wq, struct work_struct *work)
      
      // 调度延迟工作。
      int queue_delayed_work(struct workqueue_struct *wq,struct delayed_work *dwork, unsigned long delay)
      
      // 刷新指定工作队列。
      void flush_workqueue(struct workqueue_struct *wq)
      
      // 释放创建的工作队列。
      void destroy_workqueue(struct workqueue_struct *wq)
      
    • 工作队列的组织结构
      workqueue_structcpu_workqueue_structwork_struct的关系。
      一个工作队列对应一个work_queue_struct,工作队列中每cpu的工作队列由cpu_workqueue_struct表示,而work_struct为其上的具体工作。
      关系如下图所示:

      在这里插入图片描述

    • 工作队列的工作过程

      在这里插入图片描述

    • 应用

      linux各个接口的状态(up/down)的消息需要通知netdev_chain上感兴趣的模块同时上报用户空间消息。这里使用的就是工作队列。具体流程图如下所示:

      请添加图片描述

  3. 以按键进行编程实践

    3.1 按键设备驱动文件
    button_drv.c文件中,

    • 加载模块时,btn_hw_drv_probe函数中调用INIT_WORK将按键工作处理函数button_workqueue_fun赋给.func成员;
    • gpio_btn_isr按键中断服务程序(中断上半部)调用schedule_work函数,将按键.wq放入队列(链表);
    • 待中断上半部、下半部执行完毕,唤醒对应的内核线程,从队列中把work_struct结构体取出来,执行里面的函数button_workqueue_fun
/**
 * 文件    : button_drv.c
 * 作者    : glen  
 * 描述    : button driver文件
 */
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/poll.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/fs.h>
#include <linux/time.h>
#include <linux/timer.h>
#include <linux/proc_fs.h>
#include <linux/stat.h>
#include <linux/device.h>
#include <linux/module.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.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/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/fcntl.h>
#include <linux/workqueue.h>

#define KEY_BUF_LEN 16
#define NEXT_POS(x) ((x+1) % KEY_BUF_LEN)

struct gbtn_irq {
    int gpio;
    struct gpio_desc *gpiod;
    int flag;
    int irq;
    int idx;
    char kval[KEY_BUF_LEN];
    int r, w;
    struct fasync_struct *fp;
    struct timer_list key_timer;
    struct tasklet_struct tasklet;
    struct work_struct wq;
};

struct button_drv {
    struct class *class;
    struct gbtn_irq *gbtn_irq;
    
    char  *name;
    int count;
    int major;
};

static struct button_drv *btn_drv;

static int is_key_buf_empty(void *arg)
{
    struct gbtn_irq *p = (struct gbtn_irq *)arg;

    return (p->r == p->w);
}

static int is_key_buf_full(void *arg)
{
    struct gbtn_irq *p = (struct gbtn_irq *)arg;

    return (p->r == NEXT_POS(p->w));
}

static void put_key(char key, void *arg)
{
    struct gbtn_irq *p = (struct gbtn_irq *)arg;

    if (!is_key_buf_full(arg)) {
        p->kval[p->w] = key;
        p->w = NEXT_POS(p->w);
    }
}

static char get_key(void *arg)
{
    char key = 'N';
    struct gbtn_irq *p = (struct gbtn_irq *)arg;

    if (!is_key_buf_full(arg)) {
        key = p->kval[p->r];
        p->r = NEXT_POS(p->r);
    }

    return key;
}

/* 等待队列头的静态初始化 */
static DECLARE_WAIT_QUEUE_HEAD(gpio_button_wait);

static void key_timer_expire(unsigned long data)
{
    int val;
    char key;
    struct gbtn_irq *ops = (struct gbtn_irq *)data;

    /* 读取按键的值 */
    val = gpiod_get_value(ops->gpiod);

    printk("button%d %d %d\n", ops->idx, ops->gpio, val);
    key = (ops->gpio << 4) | val;

    put_key(key, ops);

    /* 唤醒等待队列 */
    wake_up_interruptible(&gpio_button_wait);

    kill_fasync(&ops->fp, SIGIO, POLL_IN);

    /* enable btn*/
    enable_irq(ops->irq);
}

/* 实现file_operations结构体成员 read 函数 */
ssize_t button_drv_read (struct file *filp, char __user *buf, size_t size, loff_t *pos)
{
    int minor = iminor(filp->f_inode);
    struct gbtn_irq *ops = (struct gbtn_irq *)filp->private_data;
    char kval;

    size = (size >= 1) ? 1 : 0;
    if (ops == NULL) {
        printk("Please register button instance %s %s, %d\n", __FILE__, __FUNCTION__, __LINE__);
        return -EIO;
    }

    if (is_key_buf_empty(ops) && (filp->f_flags & O_NONBLOCK))
        return -EAGAIN;

    wait_event_interruptible(gpio_button_wait, !is_key_buf_empty(ops));
    kval = get_key(ops);

    if (copy_to_user(buf, &kval, size))
        return -EFAULT;

    printk("Read button%d value successfully:", minor);
    return size;
}

/* 实现file_operations结构体成员 open 函数 */
int button_drv_open(struct inode *nd, struct file *filp)
{
    int ret;
    int minor = iminor(nd);
    struct gbtn_irq *ops; 
    
    if (btn_drv == NULL) {
        printk("Please register button instance %s %s, %d\n", __FILE__, __FUNCTION__, __LINE__);
        return -EIO;
    }

    ops = &btn_drv->gbtn_irq[minor];

    ret = gpiod_direction_input(ops->gpiod);
    if (ret) 
        printk("Set the button pin as input error %s %s, %d\n", __FILE__, __FUNCTION__, __LINE__);
    else 
        printk("Set the button%d pin as input successfully!\n", minor);

    filp->private_data = ops;

    return 0;
}

/* 实现file_operations结构体成员 release 函数 */
int button_drv_release (struct inode *nd, struct file *filp)
{
    struct gbtn_irq *ops = (struct gbtn_irq *)filp->private_data;

    if (ops == NULL) {
        printk("Please register button instance %s %s, %d\n", __FILE__, __FUNCTION__, __LINE__);
        return -EIO;
    }

    filp->private_data = NULL;

    return 0;
}

/* 实现file_operations结构体成员 poll 函数 */
unsigned int button_drv_poll (struct file *filp, struct poll_table_struct * wait)
{
    int minor = iminor(filp->f_inode);

    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    poll_wait(filp, &gpio_button_wait, wait);
    return (is_key_buf_empty(&btn_drv->gbtn_irq[minor]) ? 0 : POLLIN | POLLRDNORM);
}

static int button_drv_fasync(int fd, struct file *filp, int on)
{
    struct gbtn_irq *ops = (struct gbtn_irq *)filp->private_data;

	if (fasync_helper(fd, filp, on, &ops->fp) >= 0)
		return 0;
	else
		return -EIO;
}

/**
 * 1. 构造file_operations结构体 
 */
static struct file_operations button_drv_ops = {
    .owner   = THIS_MODULE,
    .read    = button_drv_read,
    .open    = button_drv_open,
    .release = button_drv_release,
    .poll    = button_drv_poll,
    .fasync  = button_drv_fasync,
};

/* 中断服务函数 */
static irqreturn_t gpio_btn_isr (int irq, void *dev_id)
{
    struct gbtn_irq *ops = dev_id;

    // printk("gpio_btn_isr key %d irq happened\n", ops->gpio);
    tasklet_schedule(&ops->tasklet);
    mod_timer(&ops->key_timer, jiffies + HZ / 50);
    schedule_work(&ops->wq);
    disable_irq_nosync(irq);
    
    return IRQ_HANDLED;
}

/* tasklet action function */
static void button_tasklet_func (unsigned long data)
{
    int val;
    struct gbtn_irq *ops = (struct gbtn_irq *)data;

    /* 读取按键的值 */
    val = gpiod_get_value(ops->gpiod);

    printk("button_tasklet_func key%d %d %d\n", ops->idx, ops->gpio, val);

}

static void button_work_func (struct work_struct *work)
{
    int val;
    struct gbtn_irq *ops = container_of(work, struct gbtn_irq, wq);

    /* 读取按键的值 */
    val = gpiod_get_value(ops->gpiod);
    printk("button_work_func: the process is %s pid %d\n", current->comm, current->pid);
    printk("button_work_func key%d %d %d\n", ops->idx, ops->gpio, val);
}

/* platform_driver结构体的 probe成员函数实现 */
int btn_hw_drv_probe (struct platform_device *pdev)
{
    int i;
    int ret;
    int count;
    // enum of_gpio_flags flag;
    struct device_node *node = pdev->dev.of_node;

    /* 从设备节点获取gpio数量 */
    count = of_gpio_count(node);
    if (!count) {
        printk("%s %s line %d, there isn't any gpio available!\n", __FILE__, __FUNCTION__, __LINE__);
        return -EIO;
    }
    
    btn_drv = kzalloc(sizeof(struct button_drv), GFP_KERNEL);
    if (btn_drv == NULL) 
        return -ENOMEM;

    btn_drv->gbtn_irq = kzalloc(sizeof(struct gbtn_irq) * count, GFP_KERNEL);
    if (btn_drv->gbtn_irq == NULL)
        return -ENOMEM;

    for (i = 0; i < count; i++) {
        btn_drv->gbtn_irq[i].gpiod = gpiod_get_index_optional(&pdev->dev, NULL, i, GPIOD_ASIS);
        if (btn_drv->gbtn_irq[i].gpiod == NULL) {
            printk("%s %s line %d, gpiod_get_index_optional failed!\n", __FILE__, __FUNCTION__, __LINE__);
            return -EIO;
        }

        btn_drv->gbtn_irq[i].irq = gpiod_to_irq(btn_drv->gbtn_irq[i].gpiod);
        btn_drv->gbtn_irq[i].gpio = desc_to_gpio(btn_drv->gbtn_irq[i].gpiod);

        setup_timer(&btn_drv->gbtn_irq[i].key_timer, key_timer_expire, &btn_drv->gbtn_irq[i]);
        btn_drv->gbtn_irq[i].key_timer.expires = ~0;
        add_timer(&btn_drv->gbtn_irq[i].key_timer);

        tasklet_init(&btn_drv->gbtn_irq[i].tasklet, button_tasklet_func, &btn_drv->gbtn_irq[i]);

        INIT_WORK(&btn_drv->gbtn_irq[i].wq, button_work_func);

        btn_drv->gbtn_irq[i].idx = i;
    }

    for (i = 0; i < count; i++) 
        /* 申请irq中断, 将中断服务程序注册到上半部 */
        ret = request_irq(btn_drv->gbtn_irq[i].irq, gpio_btn_isr, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, 
                          "gpio_btn", &btn_drv->gbtn_irq[i]);

    /* 注册file_operationss结构体对象 -- button_drv_ops  */
    btn_drv->major = register_chrdev(btn_drv->major, "gbtn", &button_drv_ops);
    btn_drv->class = class_create(THIS_MODULE, "gbtn");
    if (IS_ERR(btn_drv->class)) {
        printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
        unregister_chrdev(btn_drv->major, "gbtn");
        return PTR_ERR(btn_drv->class);
    }

    for (i = 0; i < count; i++)
        device_create(btn_drv->class, NULL, MKDEV(btn_drv->major, i), NULL, "gbtn%d", i);

    btn_drv->count = count;
        
    return 0;
}

/* platform_driver结构体的 remove成员函数实现 */
int btn_hw_drv_remove(struct platform_device *pdev)
{
    int i;
    struct device_node *node = pdev->dev.of_node;
    int count = of_gpio_count(node);

    for (i = 0; i < count; i++) {
        device_destroy(btn_drv->class, MKDEV(btn_drv->major, i));
        free_irq(btn_drv->gbtn_irq[i].irq, &btn_drv->gbtn_irq[i]);
        del_timer(&btn_drv->gbtn_irq[i].key_timer);
        tasklet_kill(&btn_drv->gbtn_irq[i].tasklet);
    }
    class_destroy(btn_drv->class);
    unregister_chrdev(btn_drv->major, "gbtn");

    kfree(btn_drv);
    return 0;
}

/* 构造用于配置的设备属性 */
static const struct of_device_id gbtns_id[] = {
    {.compatible = "glen,gbtn"},
    { },
};

/* 构造(初始化)file_operations结构体 */
static struct platform_driver btn_hw_drv = {
    .driver = {
        .name = "gbtn",
        .of_match_table = gbtns_id,
    },
    .probe = btn_hw_drv_probe,
    .remove = btn_hw_drv_remove,
};

/* 初始化 */
static int __init button_drv_init(void)
{
    int ret;
    ret = platform_driver_register(&btn_hw_drv);
    if (ret)
        pr_err("Unable to initialize button driver\n");
    else
        pr_info("The button driver is registered.\n");

    
    return 0;
}
module_init(button_drv_init);

static void __exit button_drv_exit(void)
{
    platform_driver_unregister(&btn_hw_drv);
    printk(" %s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
}
module_exit(button_drv_exit);

/* insert author information for module */
MODULE_AUTHOR("glen");
/* insert license for module */
MODULE_LICENSE("GPL");
 

probe函数采用了先获取按键节点数量,然后分别读取gpio描述符并通过其获取为gpio和irq号,并申请注册中断服务程序。

3.2 设备树文件(不作更改)

 		pinctrl_btn0:btn0 {
 			fsl,pins = <
 				MX6UL_PAD_UART1_CTS_B__GPIO1_IO18	0xF080	/* KEY0 */ 
 			>;
 		};
 
 		pinctrl_btn1:btn1 {
 			fsl,pins = <
                 MX6UL_PAD_GPIO1_IO03__GPIO1_IO03    0xF080	/* KEY1  此按键不存在 */
 			>;
 		};
     /* 在根节点下添加基于pinctrl的gbtns设备节点 */
     gbtns {
         compatible = "glen,gbtn";
         #address-cells = <1>;
 
         pinctrl-names = "default";
         pinctrl-0 = <&pinctrl_btn0 
 		             &pinctrl_btn1>;
 
         gpio-controller;
         #gpio-cells = <2>;
         gpios = <&gpio1 18 GPIO_ACTIVE_LOW /* button0 */
                  &gpio1 3 GPIO_ACTIVE_LOW>;   /* button1 */
 
     };
 
  • 取消了gpios前缀“xxx-",相应地,在驱动程序用gpiod_get_index_optional函数获取gpio描述符时,第2个形参 ”const char *con_id“ 传递NULL即可;

  • 将pinctrl-0、gpios属性值由 “<>,<>;” 改为 “<>;",效果是一样的

3.3 应用程序

应用程序文件button_drv_test.c提供:

  • 定义sig_fun信号处理函数并注册信号,以后APP收到SIGIO信号时,这个函数会被自动调用;
  • fcntl(fd, F_SETOWN, getpid()); 把APP的PID(进程ID)告诉驱动程序,这个调用不涉及驱动程序,在内核的文件系统层次记录PID;
  • oflags = fcntl(fd, F_GETFL); 读取驱动程序文件oflags
  • fcntl(fd, F_SETFL, oflags | FASYNC); 设置oflags里面的FASYNC位为1:当FASYNC位发生变化时,会导致驱动程序的fasync被调用
 /*
  * 文件名   :  button_drv_test.c
  * 作者     :  glen
  * 描述     :  button_drv应用程序
  */
 
 #include "stdio.h"
 #include "sys/types.h"
 #include "sys/stat.h"
 #include "stdlib.h"
 #include "string.h"
 #include "poll.h"
 #include <signal.h>
 #include <unistd.h>
 #include <fcntl.h>
 
 int fd;
 
 static void sig_fun(int sig)
 {
     char kval;
     read(fd, &kval, 1);
     printf("The glen button value is: %d!\n", kval);
 }
 
 /**
  * @brief   : main函数
  * @par     : argc  argv数组元素的个数
  *            argv  参数数组
  * @retval  : 0 成功    其它 失败
  */
 int main(int argc, char *argv[])
 {
     int ret;
     int oflags;
     char *filename;
 
     if (argc != 2) {
         printf("Error Usage!\r\n");
         return -1;
     }
 
     signal(SIGIO, sig_fun);
 
     filename = argv[1];
 
     /* 打开驱动文件 */
     fd = open(filename, O_RDWR);
     if (fd < 0) {
         printf("Can't open file %s\r\n", filename);
         return -1;
     }
 
     fcntl(fd, F_SETOWN, getpid());
     oflags = fcntl(fd, F_GETFL);
     fcntl(fd, F_SETFL, oflags | FASYNC);
 
     while (1) {
         sleep(2);
         printf("Read the glen button in sleepping!\n");
     }
 
     /* 关闭文件 */
     ret = close(fd);
     if (ret < 0) {
         printf("file %s close failed!\r\n", argv[1]);
         return -1;
     }
     return 0;
 }
 

3.4 在alientek_linux_alpha开发板实测验证如下

/drv_module # insmod button_drv.ko
The button driver is registered.
/drv_module # ./btn_drv_test /dev/gbtn0
./btn_drv_test: line 1: syntax error: unexpected "("
/drv_module # button_tasklet_func key0 18 0
button_work_func: the process is kworker/0:2 pid 45
button_work_func key0 18 1
button0 18 1
button_tasklet_func key0 18 1
button_work_func: the process is kworker/0:2 pid 45
button_work_func key0 18 1
button0 18 1
button_tasklet_func key0 18 0
button_work_func: the process is kworker/0:2 pid 45
button_work_func key0 18 0
button0 18 0
button_tasklet_func key0 18 1
button_work_func: the process is kworker/0:2 pid 45
button_work_func key0 18 1
button0 18 1
button_tasklet_func key0 18 0
button_work_func: the process is kworker/0:2 pid 45
button_work_func key0 18 0
button0 18 0
button_tasklet_func key0 18 1
button_work_func: the process is kworker/0:2 pid 45
button_work_func key0 18 1
button0 18 1
button_tasklet_func key0 18 0
button_work_func: the process is kworker/0:2 pid 45
button_work_func key0 18 0
button0 18 0

举报

相关推荐

0 条评论