中断处理—上、下半部分

  • 8.1 推后执行
  • 8.2 中断控制相关函数
  • 8.3 为什么要把中断分为两部分?
  • 8.4 softirqs软中断
  • 8.4.1 注册软中断处理程序
  • 8.4.2 触发软中断
  • 8.4.3 初始化软中断
  • 8.5 tasklet
  • 8.5.1 tasklet_struct结构体
  • 8.5.2 声明,tasklet初始化函数
  • 8.5.3 调度tasklet
  • 8.5.4 tasklet实现中断分层实现
  • 8.6 工作队列
  • 8.6.1 工作队列结构体
  • 8.6.2 工作队列初始化函数
  • 8.6.3 启动工作函数
  • 8.7 应当使用哪种中断bottom half



Linux中断我们需要知道两点:1、 Linux中断与中断之间不能嵌套 2、中断服务函数运行时间应当尽量短,做到快进快出。

8.1 推后执行

中断处理程序是内核必不可少的一部分。但其本身存在一些局限,这些局限包括:

  • 中断处理程序以异步方式执行并且它有可能会打断其他重要代码的执行。因此,为了避免被打断的代码停止时间过长,中断处理程序应该执行得越快越好。
  • 如果当前有一个中断处理程序正在执行,该中断线会被屏蔽,或者当前处理器上所有其他中断都会被屏蔽。因此,仍应该让它们执行得越快越好。
  • 由于中断处理程序往往需要对硬件进行操作,所以它们通常有很高的时限要求。
  • 中断处理程序不在进程上下文中运行,所以它们不能阻塞。
  • 中断处理程序必须快速、异步、简单的对硬件做出迅速响应并完成那些时间要求很严格的操作。可是,对于那些其他的、对时间要求相对宽松的任务,就应该推后到中断被激活以后再去运行。
  • 这样,整个中断处理流程就被分为了两个部分
    第一个部分是中断处理程序(上半部),内核通过对它的异步执行完成对硬件中断的即时响应。
    中断处理流程中的另外那一部分,下半部(bottom half)。

8.2 中断控制相关函数

中断处理—上、下半部分_驱动开发

8.3 为什么要把中断分为两部分?

中断服务程序异步执行,可能会中断其他的重要代码,包括其他中断服务程序。因此,为了避免被中断的代码延迟太长的时间,中断服务程序需要尽快运行.
希望限制中断服务程序所做的工作,因此处理中断的时间越短越好。
中断服务程序只作必须的工作,其他的工作推迟到以后处理。

为了解决这个问题,linux对中断的处理引入了“中断上半部”和 “中断下半部”的概念,在中断的上半部中只对中断做简单的处理,把需要耗时处理的部分放在中断下半部中,使得能够 对其他中断作为及时的响应,提供系统的实时性。这一概念又被称为中断分层。

  • “上半部分”是指在中断服务函数中执行的那部分代码,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
  • “下半部分”是指那些原本应当在中断服务函数中执行但通过某种方式把它们放到中断服务函数外执行,这样中断处理函数就会快进快出。

三种方法 : softirqs(软中断), tasklet, 和work queue

8.4 softirqs软中断

软中断的代码在kernel/softirq.c文件中;
软中断在编译内核是静态分配
每个软中断由softirq_action结构表示:

struct softirq_action
{
    void (*action)(struct softirq_action *);  软中断处理程序的函数指针
};

软中断由软件发送中断指令产生,在Linux内核中使用一个枚举变量列出了所有可用的软中断:

enum {   
	HI_SOFTIRQ=0,    	/*高优先级软中断*/
	TIMER_SOFTIRQ,    	/*定时器软中断*/
	NET_TX_SOFTIRQ,    	/*网络数据发送软中断*/
	NET_RX_SOFTIRQ,     /*网络数据接收软中断*/
	BLOCK_SOFTIRQ,    	
	BLOCK_IOPOLL_SOFTIRQ,     
	TASKLET_SOFTIRQ,    /*tasklet软中断*/
	SCHED_SOFTIRQ,    	/*调度软中断*/
	HRTIMER_SOFTIRQ,    /*高精度定时器软中断*/
	RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */     
	NR_SOFTIRQS 
};

一共10个软中断,因此NR_SOFTIRQS为10,类比硬中断,这个枚举类型列出了软中断的中断编号,我们“注册”软中断以及触发软中断都会用到软中断的中断编号。softrq_action结构体中的action成员变量就是软中断的服务函数,数组softirq_vec是个全局数组,因此所有的CPU都可以访问到,每个CPU都有自己的触发和控制机制,并且只执行自己所触发的软中断。但所执行的软中断服务函数是相同的。

8.4.1 注册软中断处理程序

void open_softirq( int nr, void (*action)(struct softirq_action *))
{
	softirq_vec[nr].action = action;
}
//将参数action赋值给数组softirq_vec[]的第nr个元素的action成员。
//参数:    nr:用于指定要“注册”的软中断中断编号    action:指定软中断的中断服务函数

8.4.2 触发软中断

软中断注册之后还需要调用触发函数触发软中断,进而执行软中断中断服务函数:

void raise_softirq(unsigned int nr); 
//参数:	 nr:要触发的软中断

8.4.3 初始化软中断

软中断必须在编译的时候静态注册,使用softirq_init函数初始化软中断:

void __init softirq_init(void)
{
	int cpu;
	for_each_possible_cpu(cpu){
		per_cpu(tasklet_vec, cpu).tail =
		&per_cpu(tasklet_vec, cpu).head;
		per_cpu(tasklet_hi_vec, cpu).tail =
		&per_cpu(tasklet_hi_vec, cpu).head;
	}
	open_softirq(TASKLET_SOFTIRQ, tasklet_action);
	open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
//softirq_init函数默认会打开TASKLET_SOFTIRQ和HI_SOFTIRQ。

8.5 tasklet

tasklet是利用软中断实现的一种下半部机制。
选择到底是用软中断还是tasklet其实很简单:
通常你应该用tasklet。就像我们在前面看到的,软中断的使用者屈指可数。它只在那些执行频率很高和连续性要求很高的情况下才需要。
而tasklet却有更广泛的用途。大多数情况下用tasklet效果都不错,而且它们还非常容易使用。
因为tasklet是通过软中断实现的,所以它们本身也是软中断。

8.5.1 tasklet_struct结构体

在驱动中使用tasklet_struct结构体表示一个tasklet:

struct tasklet_struct {    
struct tasklet_struct *next;    
unsigned long state;    
atomic_t count;    
void (*func)(unsigned long);    
unsigned long data; }; 
/* *
* next:指向链表的下一个tasklet_struct,这个参数我们不需要自己去配置。
* state:保存tasklet状态,等于0表示tasklet还没有被调度,等于TASKLET_STATE_SCHED表示tasklet被调度正准备运行。 等于TASKLET_STATE_RUN表示正在运行。
* count:引用计数器,如果为0表示tasklet可用否则表示tasklet被禁止。
* func:指定tasklet要执行的处理函数。
* data:指定tasklet处理函数的参数。
* */

8.5.2 声明,tasklet初始化函数

要使用tasklet,必须先定义一个tasklet然后使用tasket_init函数初始化tasklet:

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;
}
/**
*	 t:指定要初始化的tasklet_struct结构体
* func:指定tasklet处理函数,等同于中断中的中断服务函数
* data:指定tasklet处理函数的参数。函数实现就是根据设置的参数填充tasklet_struct结构体结构体。
**/

也可以使用宏DECLARE_TASKLET来一次性完成tasklet的定义和初始化

DECLARE_TASKLET(name, func, data)
/**
*	name:要定义的tasklet名字,这个名字就是一个tasklet_struct类型的时候变量
*	func:tasklet的处理函数
*	data:要传递给func函数的参数
**/

8.5.3 调度tasklet

在上半部,也就中断处理函数中调用tasklet_schedule函数就能使tasklet在合适的时间运行。

static inline void tasklet_schedule(struct tasklet_struct *t)
{
    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
            __tasklet_schedule(t);
}
/**
* t:tasklet_struct结构体,要调度的tasklet,也就是DECLARE_TASKLET宏里面的name。
**/

8.5.4 tasklet实现中断分层实现

实验在按键中断程序基础上完成,按键中断原本不需要使用中断分层,这里只是以它为例简单介绍tasklet的具体使用方法。 tasklet使用非常简单,主要包括定义tasklet结构体、初始化定义的tasklet结构体、实现tasklet中断处理函数、触发tasklet中断。

下面结合源码介绍如下。注意,源码是在“按键中断程序”基础上添加tasklet相关代码,这里只列出了tasklet相关代码。

/*--------------定义tasklet--------------- */
struct tasklet_struct testtasklet;  //定义全局tasklet_struct类型结构体

/*--------------tasklet处理函数-----------------*/
//定义tasklet的“中断服务函数”可以看到我们在tasklet的中断服务函数中使用延时 和printk语句模拟一个耗时的操作。
void testtasklet_func(unsigned long data)
{
    int counter = 1;
    mdelay(200);
    printk(KERN_ERR "testtasklet_func counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "testtasklet_func counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "testtasklet_func counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "testtasklet_func counter = %d \n", counter++);
    mdelay(200);
    printk(KERN_ERR "testtasklet_func counter = %d \n", counter++);
}

/*--------------中断处理函数-----------------*/
static irqreturn_t key0_handler(int irq, void *dev_id)
{
    printk(KERN_ERR "key0_irq_hander----------inter");
    /*调度tasklet*/
    tasklet_schedule(&testtasklet);

    printk(KERN_ERR "key0_irq_hander-----------exit");
    return IRQ_RETVAL(IRQ_HANDLED);
}

/*--------------解析设备树,初始化key属性并初始化中断-----------------*/  
int parser_dt_init_key(struct platform_device *pdev)
{
    /*..初始化tasklet...*/
    tasklet_init(&testtasklet, testtasklet_func, data);
    /*注册中断处理函数*/
    request_irq(priv->key.irq, priv->key.handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, priv->key.name, priv);  
}

在中断服务函数中调用tasklet_schedule函数触发tasklet中断。 在按键中断服务函数中的开始处和结束处添加打印语句,正常情况下程序会先执行按键中断的中短发服务函数, 退出中断服务函数后再执行中断的下半部分,既tasklet的“中断服务函数”。

8.6 工作队列

与软中断和tasklet不同,工作队列运行在内核线程,允许被重新调度和睡眠。 如果中断的下部分能够接受被重新调度和睡眠,推荐使用工作队列。

Work queue将下半部工作推迟给一个内核线程去执行 – work 总是运行于进程上下文.
如果推迟的工作需要睡眠,则使用work queues。否则使用softirq或tasklets。

Work queues适用于需要分配大量的内存,获得一个信号量,或者执行阻塞的I/O的情况.
“工作队列”是一个“队列”,但是对于用户来说不必关心“队列”以及队列工作的内核线程,这些内容由内核帮我们完成, 我们只需要定义一个具体的工作、初始化工作即可。和tasklet类似,从使用角度讲主要包括定义工作结构体、初始化工作、触发工作。

8.6.1 工作队列结构体

在驱动中一个工作结构体代表一个工作,工作结构体如下所示:

struct work_struct {    
	atomic_long_t data;    
	struct list_head entry;    
	work_func_t func; 	/*工作队列处理函数*/
	#ifdef CONFIG_LOCKDEP    struct lockdep_map lockdep_map; 
	#endif 
};

这些工作组织成工作队列,工作队列使用workqueue_struct结构体表示。

struct workqueue_struct {
 struct list_head pwqs; 
 struct list_head list; 
 struct mutex mutex; 
 int work_color;
 int flush_color; 
 atomic_t nr_pwqs_to_flush;
 struct wq_flusher *first_flusher;
 struct list_head flusher_queue; 
 struct list_head flusher_overflow;
 struct list_head maydays; 
 struct worker *rescuer; 
 int nr_drainers; 
 int saved_max_active;
 struct workqueue_attrs *unbound_attrs;
 struct pool_workqueue *dfl_pwq; 
 char name[WQ_NAME_LEN];
 struct rcu_head rcu;
 unsigned int flags ____cacheline_aligned;
 struct pool_workqueue __percpu *cpu_pwqs;
 struct pool_workqueue __rcu *numa_pwq_tbl[];
};

Linux 内核使用工作者线程(worker thread)来处理工作队列中的各个工作,Linux 内核使用worker 结构体表示工作者线程,worker 结构体内容如下:

struct worker {
 union {
 	struct list_head entry; 
 	struct hlist_node hentry;
 };
 struct work_struct *current_work; 
 work_func_t current_func; 
 struct pool_workqueue *current_pwq;
 bool desc_valid;
 struct list_head scheduled; 
 struct task_struct *task; 
 struct worker_pool *pool; 
 struct list_head node; 
 unsigned long last_active; 
 unsigned int flags; 
 int id; 
 char desc[WORKER_DESC_LEN];
 struct workqueue_struct *rescue_wq;
};

每个 worker 都有一个工作队列,工作者线程处理自己工作队列中的所有工作。

8.6.2 工作队列初始化函数

在实际的驱动开发中,我们只需要定义工作(work_struct)即可,关于工作队列和工作者线程我们基本不用去管。简单创建工作很简单,直接定义一个 work_struct 结构体变量即可,然后使用 INIT_WORK 宏来初始化工作

#define INIT_WORK(_work, _func) 
/**
*	_work:用于指定要初始化的工作结构体。
*	_func:用于指定工作的处理函数。
**/

也可以使用DECLARE_WOEK宏一次性完成工作的创建和初始化。

#define DECLARE_WORK(n,f)
/**
*	n:表示定义的工作
*	f:表示工作对应的处理函数
**/

8.6.3 启动工作函数

驱动工作函数执行后相应内核线程将会执行工作结构体指定的处理函数,驱动函数如下所示。

static inline bool schedule_work(struct work_struct *work)
/**
*	work:要调度的工作
**/

8.7 应当使用哪种中断bottom half

  • 从设计上讲,Softirq提供最少的顺序保证.
  • 这需要Softirq处理函数采取一些额外的步骤保证数据安全,因为两个以上的同类型softirqs只能同时运行于不同的CPU。
  • Softirq多用于时间要求严格和使用频度高的场合
  • 如果代码不能很好地线程化,tasklet意义较大
  • Tasklets 有一个简单的接口,由于两个同类型的不能同时运行,他们非常易于实现。
  • 如果你的延期的工作需要运行于进程上下文, 唯一的选择是work queue.