当前位置: 首页 > 工具软件 > Tasklet > 使用案例 >

Linux中断——tasklet、workqueue

司空朝
2023-12-01

目录

一、takslet

1.1、数据结构

1.2、初始化

1.3、执行tasklet

1.4、启用和禁用tasklet

1.5、销毁tasklet

sample code:

二、workqueue

2.1、创建工作队列

2.2、调度

2.3、取消调度

2.4、销毁workqueue

sample code:

三、tasklet与workqueue不同

Tasklet与workqueue的不同应用环境总结如下:

Tasklet和workqueue如何选择?

引申

原子操作


tasklet和workqueue都属于内核的延期工作(deferred work),也属于中断的范畴,处在中断的bottom half(前面文章《Linux中断——request_irq》介绍过top half)。

一、takslet

tasklet拥有的一些特征:

  1. 可以反复启用、禁用,在多次禁用之后必须先启用才能作用;
  2. tasklet可以嵌套,也就是它可以注册自己;
  3. 可以指定优先级;
  4. 当系统负载小的时候响应是即时的,相反系统busy的时候,也不会超过一个timer;
  5. 可能会跟其他tasklet产生并发,但是跟自己是严格序列化的,所以同一个tasklet不会同时在多个CPU运行。

1.1、数据结构

struct tasklet_struct
{
	struct tasklet_struct *next;
	unsigned long state;
	atomic_t count;
	void (*func)(unsigned long);
	unsigned long data;
};

 

1.2、初始化

  • 动态(run time)创建

void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long),  unsigned long data);

void tasklet_init(struct tasklet_struct *t,
		  void (*func)(unsigned long), unsigned long data)
{
	t->next = NULL;
	t->state = 0;
	atomic_set(&t->count, 0);
	t->func = func;
	t->data = data;
}
  • 静态(build time)创建

DECLARE_TASKLET(name, func, data)

DECLARE_TASKLET_DISABLED(name, func, data)

name 一般为声明的数据结构;func为任务函数,data为传给func的数据。

以上两者差别:

DECLARE_TASKLET(name,func, data) 初始化的引用计数器count=0,小任务处于激活状态

DECLARE_TASKLET_DISABLED(name,func, data) 初始化的引用计数器count=1,小任务处于禁止状态

 

 

1.3、执行tasklet

static inline void tasklet_schedule(struct tasklet_struct *t);

linux/include/linux/interrupt.h
static inline void tasklet_schedule(struct tasklet_struct *t)
{
	if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
		__tasklet_schedule(t);
}


linux/kernel/softirq.c
void __tasklet_schedule(struct tasklet_struct *t)
{
	unsigned long flags;

	local_irq_save(flags);
	t->next = NULL;
	*__get_cpu_var(tasklet_vec).tail = t;
	__get_cpu_var(tasklet_vec).tail = &(t->next);
	raise_softirq_irqoff(TASKLET_SOFTIRQ);
	local_irq_restore(flags);
}


/*
 * This function must run with irqs disabled!
 */
inline void raise_softirq_irqoff(unsigned int nr)
{
	__raise_softirq_irqoff(nr);

	/*
	 * If we're in an interrupt or softirq, we're done
	 * (this also catches softirq-disabled code). We will
	 * actually run the softirq once we return from
	 * the irq or softirq.
	 *
	 * Otherwise we wake up ksoftirqd to make sure we
	 * schedule the softirq soon.
	 */
	if (!in_interrupt())
		wakeup_softirqd();
}

static inline void tasklet_hi_schedule(struct tasklet_struct *t);

void __tasklet_hi_schedule(struct tasklet_struct *t)
{
	unsigned long flags;

	local_irq_save(flags);
	t->next = NULL;
	*__get_cpu_var(tasklet_hi_vec).tail = t;
	__get_cpu_var(tasklet_hi_vec).tail = &(t->next);
	raise_softirq_irqoff(HI_SOFTIRQ);
	local_irq_restore(flags);
}

从代码的实现中可以看到tasklet_hi_scheduletasklet_schedule唯一差别是在调用raise_softirq_irqoff中指定的参数不一样,tasklet_schedule指定的是TASK_SOFTIRQ,而tasklet_hi_schedule指定的是HI_SOFTIRQ。表现出来的效果是它优先级更高,只有那种对latency要求小的情况才用tasklet_hi_schedule,正常情况都使用普通tasklet_schedule。可以通过/proc/jitasklethi看状态。

 

1.4、启用和禁用tasklet

static inline void tasklet_disable_nosync(struct tasklet_struct *t);

static inline void tasklet_disable(struct tasklet_struct *t);

static inline void tasklet_enable(struct tasklet_struct *t);

static inline void tasklet_hi_enable(struct tasklet_struct *t);

当没有使用disable就无需执行enable,调用tasklet_schedule之后直接执行。

tasklet_disable_nosync和tasklet_disable的差别在于:tasklet_disable_nosync不等待当前正在执行的tasklet而直接disable掉,但是它无法disable运行在其他CPU上的tasklet;被tasklet_disable作用的tasklet如果是正在运行,它会忙等待它退出,但是退出之后被tasklet_schedule进来的tasklet依然会在enable之后作用,而不是丢弃。

tasklet_enable和tasklet_hi_enable仅仅在优先级上有差异,如果是一个任务被tasklet_schedule了,但是当前处于disable状态,如果被enable之后会第一时间自动执行而无需再次执行tasklet_schedule

 

1.5、销毁tasklet

void tasklet_kill(struct tasklet_struct *t)

它一般在设备关闭或者驱动卸载的时候被执行,如果当前task正在执行,它会等待执行完后销毁。如果tasklet有调用它本身,那就要注意在执行tasklet_kill时必须先阻止它调用自己。

sample code:

#include<linux/init.h>
#include<linux/fs.h>
#include<linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/kernel.h>
#include<linux/interrupt.h>
 
static struct t asklet_struct my_tasklet;
 
static void tasklet_handler (unsigned longd ata)
{
    printk(KERN_ALERT,"tasklet_handler is running./n");
}
 
staticint __init test_init(void)
{
    tasklet_init(&my_tasklet,tasklet_handler,0);
    tasklet_schedule(&my_tasklet);
    return 0;
}
 
static void __exit test_exit(void)
{
    tasklet_kill(&tasklet);
    printk(KERN_ALERT,"test_exit is running./n");
}
MODULE_LICENSE("GPL");
 
module_init(test_init);
module_exit(test_exit);

 

二、workqueue

workqueue拥有的一些特征:

  1. 在一个处理上下文中(process context)可以做sleep操作;
  2. 禁止与用户空间进行数据拷贝(如果非要做,可以用内存映射、DMA)。
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
};

struct workqueue_struct {
	struct cpu_workqueue_struct *cpu_wq;
	struct list_head list;
	const char *name;
	int singlethread;
	int freezeable;		/* Freeze threads during suspend */
#ifdef CONFIG_LOCKDEP
	struct lockdep_map lockdep_map;
#endif
};

 workqueue——work——handlefunc

注意这三者的关系,workqueue是一个类,work是一个成员,handlefunc是具体的事件

2.1、创建工作队列

将work与对应的handlefunc建立联系

  • 动态(run time)创建

INIT_WORK(struct work_struct *work, void (*function)(void *), void *data)

PREPARE_WORK(struct work_struct *work, void (*function)(void *), void *data)

  • 静态(build time)创建

DECLARE_WORK(name, void, (*function)(void *), void *data)

  • 初始化

static struct workqueue_struct *create_workqueue(const char *name);
static struct workqueue_struct *create_freezeable_workqueue(const char *name);
static struct workqueue_struct *create_singlethread_workqueue(const char *name);

2.2、调度

将workqueue与work建立联系

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);

以上函数实现将对应的work加入到指定的workqueue中。如果返回0,表示成功加入队列中;返回非零,表示相同的work已经在队列中。

但是有两个注意点:

  1. 因为它运行在内核线程中,所以它无法访问用户空间数据;
  2. 虽然它可以执行sleep操作,但是它会影响其他加入相同workqueue的task。

针对多核CPU的调度

int queue_work_on(int cpu, struct workqueue_struct *wq, struct work_struct *work);

int queue_delayed_work_on(int cpu, struct workqueue_struct *wq, struct delayed_work *dwork, unsigned long delay)

 

2.3、取消调度

int cancel_delayed_work_sync(struct delayed_work *dwork);

int cancel_work_sync(struct work_struct *work);

返回0,表示它正在执行;返回非0,表示已经被取消;

注意点:

  • cancel_work_sync会取消当前等待队列中的任务,如果work的回调显示在执行,cancel_work_sync会阻塞直到它完成;
  • 即时当前工作队列迁移到另一个工作队列中也可以取消;
  • 它只能在调用它之后,当前workqueue中的指定work,如果这个时候有新增work到workqueue中,它无法取消;
  • cancel_work_sync只在timer没有挂起的时候作用,否则进入busy-wait循环直到计时器时间片到了;
  • 使用者在使用过程中要确保函数返回前,workqueue_struct不被销毁。

void flush_workqueue(struct workqueue_struct *wq);

如果在执行cancel之后返回0,如过要确认执行的工作队列完毕后再进行下一步操作该怎么办?通过调用flush操作会等待当前工作队列完成后返回,但是如果有新的工作队列被执行,将不受它监控。

2.4、销毁workqueue

void destroy_workqueue(struct workqueue_struct *wq);

安全结束一个workqueue,但是所有被阻塞的work会被先执行完。注意是workqueue不是work

 

sample code:

#include<linux/module.h>  
#include<linux/init.h>   
#include<linux/workqueue.h>   
  
staticstruct workqueue_struct *queue =NULL;  
staticstruct work_struct work;  
  
staticvoid work_handler(struct work_struct*data)  
{  
    printk(KERN_ALERT"work handler function./n");  
}  
  
staticint __init test_init(void)  
{  
    queue= create_singlethread_workqueue("helloworld"); /*创建一个单线程的工作队列*/  
    if(!queue)  
        goto err;  
  
    INIT_WORK(&work, work_handler);  
    schedule_work(&work);/*schedule_work是添加到系统的events workqueue, 要添加到自己的    workqueue, 应该使用queue_work, 故此处有误*/  
  
    return 0;  
err:  
    return-1;  
}  
  
staticvoid __exit test_exit(void)  
{  
    destroy_workqueue(queue);  
}  
MODULE_LICENSE("GPL");  
module_init(test_init);  
module_exit(test_exit);

另外还有一种shared workqueue,它使用比较少,会对具体workfunc有一些限制,比如不建议使用sleep,因为当它sleep,其它共享的将无法使用。另外接口函数缺少struct workqueue_struct *wq成员。由于使用比较少,这里就不介绍。

三、tasklet与workqueue不同

tasklet与workqueue非常相似,都属于内核的一种延期工作操作,但是两者还是有一些差异的

  1. tasklet要求处理必须是原子性的要求工作在中断上下文,而workqueue是可以将工作给一个内核线程上下文去执行,而且可以执行sleep操作,工作与进程上下文;
  2. work queue可以指定一个时间之后去执行对应的操作,而tasklet不行。

tasklet

Workqueue

处于atomic context,不能sleep

不处于atomic context,可以sleep

处于中断上下文,OS不可以进行进程调度

处于进程上下文,OS可以进行进程调度

运行调度它们的同一个CPU上

默认同一个CPU上

不能指定确定时间进行调度

不能指定确定时间进行调度或者指定至少延时一个确定时间后调度

只能交给ksoftirqd/0

可以提交给events/0,也可以提交给自定义的workqueue

Tasklet函数带参数

Work函数不带参数

 

Tasklet与workqueue的不同应用环境总结如下:

(1) 必须立即进行紧急处理的极少量任务放入在中断的top half(request_irq)中,此时屏蔽了与自己同类型的中断,由于任务量少,所以可以迅速不受打扰地处理完紧急任务。

(2) 需要较少时间的中等数量的急迫任务放在tasklet中。此时不会屏蔽任何中断(包括与自己的顶半部同类型的中断),所以不影响顶半部对紧急事务的处理;同时又不会进行用户进程调度,从而保证了自己急迫任务得以迅速完成。

(3) 需要较多时间且并不急迫(允许被操作系统剥夺运行权)的大量任务放在workqueue中。此时操作系统会尽量快速处理完这个任务,但如果任务量太大,期间操作系统也会有机会调度别的用户进程运行,从而保证不会因为这个任务需要运行时间将其它用户进程无法进行。

(4) 可能引起睡眠的任务放在workqueue中。因为在workqueue中睡眠是安全的。

Tasklet和workqueue如何选择?

  • 是否sleep?如果延时执行的任务需要sleep,那么就选择workqueue。如果延时执行的任务不需sleep,那么就选择tasklet
  • 如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用workqueue。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时,它都会非常有用。如果不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet。

引申

原子操作

atomic是原子的意思,意味"不可分割"的整体。在Linux kernel中有一类atomic操作API。这些操作对用户而言是原子执行的,在一个CPU上执行过程中,不会被其他CPU打断。最常见的操作是原子读改写,简称RMW。简单的例子是,一个普通的变量递增操作,在多核同时参数的情况下,会导致与预期结果不一致。atomic实现原理 介绍了三种实现方式Bus Lock、Cacheline Lock、LL/SC

 

 

 

 

 类似资料: