linux设备驱动中的中断与时钟

1. 中断与定时器

中断是指CPU在执行程序过程中,出现了某些突发事件紧急处理,CPU必须暂停当前程序的执行,转去执行突发事件,处理完毕后有返回到原程序被中断的位置继续执执行。根据中断的来源可以分为外部中断和内部中断,内部中断是由CPU内部引起的,外部中断是来自CPU外部,由外设引起。根据中断是否能够屏蔽,中断可以分为可屏蔽中断和不可屏蔽中断,可屏蔽中断可以通过设置中断屏蔽寄存器的方式被屏蔽,不可屏蔽中断不能被屏蔽。根据中断入口跳转方式的不同,中断可以分为向量中断和非向量中断,向量中断CPU为每隔中断分配不同的中断入口地址,非向量中断是多个中断共享一个中断入口地址,然后由软件确定中断的入口。

在ARM多核处理器中通常采用的是中断控制器GIC,它支持以下三种类型的中断:

SGI: 软件产生的中断,可用于多核的核间通信,一个CPU通过写GIC的寄存器给另一个CPU产生中断。

PPI: 私有外设中断,外设中断只能发送给绑定的CPU。

SPI: 共享外设的中断,这种中断可以路由给任何一个CPU。

2. linux中断处理程序架构

设备中断会打断内核进程的正常调度和运行,中断服务程序应该尽可能的小,但实际情况是在中断中可能需要完成大量的耗时的处理。为了找到一个平衡点,linux中断处理程序一般分为顶半部和底半部。顶半部一般完成较紧急的功能,通常是对寄存器状态的读取,并清除中断标志就把交给挂到低半部中断处理程序的队列中。底半部一般进行比较耗时的处理,并能够被更高级的中断打断。

3. linux中断编程

3.1 申请和释放中断

(1) 申请riq
int request_irq(unsigned int irq,irq_handler_t handler,unsigned long flags,const char *name,void *dev);
irq: 申请的硬件中断号。
handler: 中断处理函数,当发生中断时调用该函数。
flags: 中断处理的属性,可以指定触发方式和处理方式。触发方式有IRQ_TRIGGER_RISING、IRQ_TRIGGER_FALLING、IRQ_TRIGGER_HIGH、IRQ_TRIGGER_LOW等。处理方式有IRQ_SHARED(共享中断)。
dev: 传递给中断服务程序的私有数据,一般为这个设备的设备结构体或NULL。
返回值:返回0表述成功,返回-EINVL表示中断号无效或处理函数为NULL,返回-EBUSY表示中断已经被占用且不能共享。

int devm_request_irq(struct device *dev,unsigned int irq,irq_handler_t handler,unsigned long flags,const char *name,void *dev);
一般不需要出错处理和显示remove()接口里显式释放,类似JAVA的垃圾回收机制。
(2) 释放irq
void free_irq(unsigned int irq,void *dev_id);

3.2 使能和屏蔽中断

(1) 屏蔽中断源
void disable_irq(int irq); //等待中断处理完成,返回。
void disable_irq_nosync(int irq);//立即返回。
void enable_irq(int irq); //使能中断。
(2) 函数(宏)屏蔽本CPU内部的所有中断
local_irq_save(flags) //把目前的中断状态保留进flags。
local_irq_restore(flags) //恢复

3.3 底半部中断

linux实现底半部中断的主要机制有tasklet、工作队列、软中断、线程化irq。

1. tasklet
执行的上下文是软中断,执行的时机是顶半部返回的时候。定义一个tasklet结构my_tasklet和中断处理函数my_tasklet_func通过DECLARE_TASKLET(my_tasklet,my_tasklet_func),把他们关联起来。tasklet_schedule()函数能在系统适当的时间调度tasklet的运行。
2. 工作队列
执行上下文是内核线程,可以调度和睡眠。
struct work_struct my_wq; //定义一个工作队列
void my_wq_func(struct work_struct *work); //定义处理函数
通过INIT_WORK(my_work,my_wq_func)初始化工作队列并将工作队列和处理函数绑定。
schedule_work(); 调度工作队列执行。
3. 软中断
软中断是传统的底半部处理机制,执行时机是顶半部返回的时候,运行在中断上下文,tasklet也是基于软中断实现的。在linux内核中,用softirq_action结构体表征一个软中断,此结构体包含软中断处理函数指针和传递给该函数的参数。使用open_softirq()函数可以注册软中断对应的处理函数,而raise_softirq()函数可以触发一个中断。因为tasklet和软中断运行在中断上下文所以不允许睡眠,而工作队列运行在进程上下文所以允许睡眠。
local_bh_disable()和local_bh_enable()是内核禁止和使能软中断tasklet底半部中断的机制。
内核中采用的softirq的地方包括HI_SOFIRQ、TIME_SOFIRQ、NET_TX_SOFIRQ、NET_RX_SOFIRQ、SCSI_SOFIRQ、TASKLET_SOFTRIQ等。硬中断、软中断和信号的区别:硬中断是外部中断设备对CPU的中断,软中断是中断底半部的处理机制,信号是内核对某个进程的中断。中断的优先级高于任何一个软中断,软中断高于任何一个线程的。软中断适度的线程化可以缓解高负载情况下的系统反应。
4. threaded_irq
int request_threaded_irq(unsigned int irq,irq_handler_t handler,irq_handler_t thread_fn,unsigned long flags,const char *name,void *dev);//申请中断
int devm_request_threaded_irq(struct device *dev,unsigned int irq,irq_handler_t handler,irq_handler_t thread_fn,unsigned long flags,const char *name,void *dev);//申请中断,自动清理
在申请中断的时间内核会为相应的中断分配一个对应的内核线程,次线程只针对此中断号,不同的中断会得到不同的线程。参数handler对应的函数执行在中断上下文,thread_fn执行于线程上下文,若handler返回IRQ_WAKE_THREAD,内核会调度对应的线程执行thread_fn函数。这两个函数支持在flags中设置IRQF_ONESHOT标记,这样内核会自动帮助我们在中断上下文中屏蔽对应的中断号,在内核调度thread_fn执行的时间重新使能该中断。IRQF_ONESHOT避免了中断服务程序已退出,中断就洪泛的情况。

4. 共享中断

共享中断是多个设备共享一根硬件中断线,linux支持这种中断共享。中断共享的使用方法:
(1) 共享中断的多个设备在申请中断时,都应该使用IRQF_SHARED标志,而且一个设备以IRQF_SHARED方式申请中断成功的前提是该中断没有被申请或申请该中断的所有设备都是以IRQF_SHARED的方式申请的该中断。
(2) 尽管内核模块能访问的全局地址都可以作为request_irq()的最后一个参数,但是设备结构体指针是传入的最佳参数。
(3) 在中断到来时,会遍历执行共享此中断的所有中断处理程序,知道一个函数返回IRQ_HANDLED。在中断处理程序的顶半部中,应根据硬件寄存器的信息比照传入的dev_id参数迅速的判断是否为本设备的中断,若不是则返回IRQ_NONE。

5. 内核定时器

5.1 内核定时器编程

软件意义上的定时器最终依赖硬件定时器来实现,内核在时钟发生中断后检测各个定时器是否到期,到期后的定时器处理函数将作为软中断在底半部执行。实质上,时钟中断处理程序会唤起TIME_SOFTIRQ软中断,运行当前处理器到期的所有时钟。
linux内核中提供的操作定时器的数据和结构的函数如下:
1. timer_list
struct timer_list{
          struct list_head entry; 
          unsigned long  expires;  //定时器到期时间
          struct tvec_base *base;
          void (*function) (unsigned long); //定时器中断处理函数
          unsigned long data;  //作为参数传入function
          int slack;
          ...
};
2. 初始化定时器(宏)
init_timer(struct timer_list *timer);//初始化timer_list中的entry的next为NULL,并给base赋值。
TIMER_INITALIZER(_function,_expires,_data); //初始化定时器
DEFINE_TIMER(_name,_function,_expires,_data);//初始化定时器
3. 增加定时器
void add_timer(struct timer_list *timer); //将定时器加入到内核动态定时器链表中。
4. 删除定时器
int del_timer(struct timer_list *timer); //删除定时器
int del_timer_sync(struct timer_list *timer); //删除定时器(同步)
5. 修改定时器
int mod_timer(struct timer_list *timer,unsigned long expires);//修改定时器到期的时间

5.2 内核中延时的工作delayed_work

对于周期性的任务除了定时器外还有delayed_work,其本质是利用工作队列和定时器实现。
struct delayed_work{
           struct work_struct work;
           struct timer_list timer;
           struct workqueue_struct *wq;
           int cpu;
};
int schedule_delayed_work (struct delayed_work *work,unsigned long delay);当指定的delay到来时,delayed_work结构体中的work成员work_func_t类型成员func()会被执行。其常见用法schedule_delayed_work (&work,msecs_to_jiffies(xxxx));
取消delayed_work的函数有:
int cancel_delayed_work(struct delayed_work *work);
int cancel_delayed_work_sync(struct delayed_work *work);//同步

6. 内核延时

6.1 短延时

linux内核提供3个函数分别进行纳秒、微妙、毫秒的延时:
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
其原理是忙等待,根据CPU的频率进行一定次数的循环。
对于毫秒的延时最好不要直接使用mdelay()函数,对于毫秒以上的延时内核提供以下函数:
void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int milisecs); //可以被中断打断
void ssleep(unsigned int seconds);

6.2 睡着延时

睡着延时是在等待时间到来之前进程处于睡眠状态,CPU资源被其他进程使用。schedule_timeout()可以使当前睡眠的进程在制定的jiffies之后再次被调度执行。其原理是向系统添加一个定时器,在定时器处理函数中唤醒与参数对应的进程。

sleep_on_timeout(wait_queue_head_t *q,unsigned long timeout); //超时前不能被中断打断
interruptible_sleep_on_timeout(wait_queue_head_t *q,unsigned long timeout); //超时前能被中断打断
这两个函数把当前进程添加到等待队列上,从而在等待队列上睡眠。当超时发生时,进程将被唤醒。





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值