【linux学习笔记】中断上下文

中断是我们的老朋友了,在裸机开发中中断处于不可或缺的地位。在linux开发中,也有一套自己的中断体系。与裸机开发最大的不同是,操作系统通常将中断划分为了上下文。比如上文处理硬件请求,而下文则对数据进行处理,网卡是一个典型的例子。

中断上文

相关函数接口

申请一个中断:

int request_irq(unsigned int irq, 
				irqreturn_t (*handler)(int, void *, struct pt_regs *),
		 		unsigned long irq_flags, 
		 		const char * devname, 
		 		void *dev_id)

参数分别是中断号、中断处理程序入口、flag(书的94页)、设备名称(可查询)、dev_id用于共享中断线(没有的话写NULL)。
这个函数可能会引起休眠,因此不能在中断上下文中调用(原因可能是需要申请空间,用到了kmalloc)。

irqreturn_t (*handler)(int irq, void *dev, struct pt_regs *)

irqreturn_t 返回值表明产生的这个中断是否是这个处理函数所对应的设备。三个参数分别是中断号、共享中断线模式下的设备、用于查看状态保存寄存器值的空间入口。

中断发生的过程

裸机的中断处理过程相当简单:中断发生->CPU响应中断(期间会关闭中断线使能)->保存当前断点和现场->跳转到中断服务程序入口位置执行用户代码,如果支持中断嵌套,此处要将中断重新开启->执行完毕,跳转回到断点。
无论是ucos还是linux,执行中断的最后都要判断是否还要任务调度,这是操作系统与裸机最大的不同。
流程:
1.中断发生->CPU响应中断->保存当前断点和现场,以上过程通常是汇编实现;
2.调用do_IRQ

fastcall unsigned int do_IRQ(struct pt_regs *regs)

do_IRQ对接收到的中断进行应答,并关闭这条中断线;寻找这条中断线上是否有合适的中断处理函数,如果有就调用handle_IRQ_event()
3.handle_IRQ_event函数原型:

irqreturn_t handle_IRQ_event(unsigned int irq, 
							struct irqaction *action)

这个函数根据之前的irq_request()中的flag,判断是否需要重新开启中断;根据是否属于共享中断线,调用在这条中断线上的全部中断服务程序;完成后返回do_irq;
4.do_irq最后的任务是对任务进行调度,如果发现任务调度已经挂起,也就是need_resched标志位已经置位的时候,判断此时内核的方向:
a)如果正在返回用户空间(也就是之前中断的是用户进程),直接调用schedule();
b)反之,如果之前是内核中断了自己的进程,首先就是判断调度任务是否安全(也就是之前被中断的进程是否还持有锁),如果安全则调用shcedule(),否则只能返回断点。

卸载一个中断处理函数:

void free_irq(unsigned int irq, void *dev_id)

显然,从参数上来看,这个函数的作用就是根据dev_id卸载掉中断线上的一个处理函数。

使能/屏蔽中断

关闭单个中断源

disable_irq(n);
disable_irq_nosync(n);
enable_irq(n);

n表示中断号,disable_irq和disable_irq_nosync区别在于前者会等中断处理完成之后再关闭中断,而后者直接关闭中断。这就意味着在中断上半部调用disable_irq会造成死锁。

关闭全部的中断源

local_irq_disable(void);
local_irq_save(flag);
-----------------------------
local_irq_enable(void);
local_irq_restore(flag);

flag:这个和ucos中一样,保存了之前中断源的状态用于之后的restore函数恢复只用,所以需要提前定义,是一个ul类型数。

中断下文

目前的内核中,中断推后工作实现的机制有三种:
1)tasklet
tasklet_struct:

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

申请一个tasklet由宏完成。

#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

func、data分别是tasklet代码、和这段代码的参数。
调度一个tasklet需要在上半部调用:

tasklet_schedule(struct tasklet_struct *t)

2)工作队列
同样是用宏申请、初始化一个工作队列结构体work_struct:

INIT_WORK(_work, _func)	

参数分别是work_struct结构体和工作队列的回调函数。
调度函数是:

schedule_work(struct work_struct *work)

3)软中断
open_softirq()申请注册一个软中断;
raise_softirq()触发一个软中断
在内核中,软中断使用范围很小,几乎不用,更倾向于使用基于软中断实现的tasklet

如何选择:
  • 软中断:特点是效率高,但是缺乏执行顺序保障(允许相同类别的软中断在不同处理器上执行)。因此,对于网络子系统,因为其短时间内会产生大量的中断,且只会使用单个内核,所以适合采用这种机制。需要在编译的时候静态创建。
  • tasklet:这种机制是在中断下文机制选择上优先考虑的。接口简单,不允许相同类型的tasklet同时执行。可以由代码动态创建。
  • 工作队列:如果下文中出现必须睡眠的情况,也就是将下文推后到进程上下文中执行,工作队列是唯一选择。涉及到内核线程、上下文切换,因此消耗极大。
区分硬中断、软中断、信号

比较接近的是硬中断与信号,前者是由外设产生,中断对象是CPU;
后者的产生对象是内核或者进程,中断对象是另一个进程,是内核对硬中断机制的模范;
软中断是中断下半部的一种处理机制。这里要注意swi也可以叫做软中断,但是这个软中断是由软件产生的中断进而发起系统调用实现由用户空间到内核空间。

其他方式

ucos上没有软中断,任务队列,tasklet但是依然可以实现中断上下文的划分,采用的方法是信号量,消息队列,消息邮箱等。
所以,也可以使用这种方式实现中断上下文,如信号量、互斥体,等待队列等,前提是要求这些进程正处于挂起状态。

你可能感兴趣的:(linux学习)