工作延迟机制/中断底半步机制

中断为什么分为顶半部和底半部?

我们的理想是中断处理时间尽量短,但现实是中断往往要处理大量工作量,在现实和理想之间寻求最佳平衡点,引入顶半部和底半部机制,在顶半部实现重要且时间短的工作,在底半部实现大量的工作量,顶半部是不可被中断打断,而底半部可被中断打断。

底半部机制实现:

1.软中断 运行在中断上下文
2.tasklet 运行在中断上下文
3.工作队列 运行在进程上下文

顶半部指的是中断处理程序,底半部则指的是一些虽然与中断有相关性但是可以延后执行的任务,底半步可以被中断打断

举个例子:在网络传输中,网卡接收到数据包这个事件不一定需要马上被处理,适合用下半部去实现;但是用户敲击键盘这样的事件就必须马上被响应,应该用中断实现。
两者的主要区别在于:中断不能被相同类型的中断打断,而下半部依然可以被中断打断;中断对于时间非常敏感,而下半部基本上都是一些可以延迟的工作。由于二者的这种区别,所以对于一个工作是放在上半部还是放在下半部去执行,可以参考下面4条:
如果一个任务对时间非常敏感,将其放在中断处理程序中执行。
如果一个任务和硬件相关,将其放在中断处理程序中执行。
如果一个任务要保证不被其他中断(特别是相同的中断)打断,将其放在中断处理程序中执行。
其他所有任务,考虑放在底半部去执行。

注: 软中断在实际底层开发中,直接使用的情况较少,这里只说tasklet和工作队列

tasklet:tasklet是软中断的一种特殊用法,即延迟情况下的串行执行,不能阻塞和睡眠

由于软中断必须使用可重入函数,这就导致设计上的复杂度变高,作为设备驱动程序的开发者来说,增加了负担。而如果某种应用并不需要在多个CPU上并行执行,那么软中断其实是没有必要的。因此诞生了弥补以上两个要求的tasklet。它具有以下特性:
a)一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行。
b)多个不同类型的tasklet可以并行在多个CPU上。
c)软中断是静态分配的,在内核编译好之后,就不能改变。但tasklet就灵活许多,可以在运行时改变(比如添加模块时)。
tasklet是在两种软中断类型的基础上实现的,因此如果不需要软中断的并行特性,tasklet就是最好的选择。

使用示例

void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data)//动态初始化tasklet
static inline void tasklet_disable(struct tasklet_struct *t) //函数暂时禁止给定的tasklet被tasklet_schedule调度,直到这个tasklet被再次被enable;若这个tasklet当前在运行, 这个函数忙等待直到这个tasklet退出
static inline void tasklet_enable(struct tasklet_struct *t) //使能一个之前被disable的tasklet;若这个tasklet已经被调度, 它会很快运行。tasklet_enable和tasklet_disable必须匹配调用, 因为内核跟踪每个tasklet的"禁止次数"
static inline void tasklet_schedule(struct tasklet_struct *t) //调度 tasklet 执行,如果tasklet在运行中被调度, 它在完成后会再次运行; 这保证了在其他事件被处理当中发生的事件受到应有的注意. 这个做法也允许一个 tasklet 重新调度它自己
tasklet_hi_schedule(struct tasklet_struct *t) //和tasklet_schedule类似,只是在更高优先级执行。当软中断处理运行时, 它处理高优先级 tasklet 在其他软中断之前,只有具有低响应周期要求的驱动才应使用这个函数, 可避免其他软件中断处理引入的附加周期.
tasklet_kill(struct tasklet_struct *t) //确保了 tasklet 不会被再次调度来运行,通常当一个设备正被关闭或者模块卸载时被调用。如果 tasklet 正在运行, 这个函数等待直到它执行完毕。若 tasklet 重新调度它自己,则必须阻止在调用 tasklet_kill 前它重新调度它自己,如同使用 del_timer_sync

static struct crq_server_adapter ibmvmc_adapter;
struct tasklet_struct *work_task;
static void ibmvmc_task(unsigned long data)//底半部函数
{
    /*自定义处理函数*/
}

static irqreturn_t ibmvmc_handle_event(int irq, void *dev_instance)
{
    /*自定义中断处理程序*/
	tasklet_schedule(&work_task);//调用注册的底半部函数
	return IRQ_HANDLED;
}

static int ibmvmc_probe(struct vio_dev *vdev, const struct vio_device_id *id)
{
	struct crq_server_adapter *adapter = &ibmvmc_adapter;
	tasklet_init(&work_task, ibmvmc_task, (unsigned long)adapter);
	request_irq(vdev->irq, ibmvmc_handle_event,	0, "ibmvmc", (void *)adapter);
    return 0;
err:  
	tasklet_kill(&work_task);
	return -ENOMEM;

}

static int ibmvmc_remove(struct vio_dev *vdev)
{
	tasklet_kill(&work_task);
    return 0;
}

工作队列:工作队列允许重新调度甚至是睡眠

使用以下规则:

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

缺省工作队列

静态创建
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。
以上均是采用缺省工作者线程来实现工作队列,其优点是简单易用,缺点是如果缺省工作队列负载太重,执行效率会很低,这就需要我们创建自己的工作者线程和工作队列。

自定义工作队列

create_workqueue(name)
//宏定义 返回值为工作队列,name为工作线程名称。创建新的工作队列和相应的工作者线程,name用于该内核线程的命名。

int queue_work(struct workqueue_struct *wq, struct work_struct *work)
//类似于schedule_work,区别在于queue_work把给定工作提交给创建的工作队列wq而不是缺省队列。

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)
//释放创建的工作队列。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值