10.1 中断与定时器
- 分类
- 中断来源
- 内部中断
- 来源于CPU的内部(软件中断的指令、溢出、除法错误等)
- 外部中断
- 来源于外设请求
- 内部中断
- 是否可屏蔽
- 可屏蔽中断
- 不可屏蔽中断
- 中断的入口方式
- 向量中断
- CPU给每个不同的中断分配不同的中断号,中断发生时会自动跳到该中断号对应的地址执行
- 非向量中断
- 多个中断共享一个入口地址,进入该入口后通过软件判断中具体哪个中断
- 向量中断
- ARM渡河处理器里最常用的中断控制器是GIC,它支持三种类型的中断
- SGI:软件产生的中断,可用于多核间通信,一个CPU可以通过写GIC寄存器给另一个CPU产生中断
- PPI:某个CPU私有外设的中断,这类外设中断只能发给一个CPU
- SPI:共享外设中断,这类外设的中断可以路由到任何一个CPU
- 中断来源
- ARM Linux默认情况下,中断都是由CPU0上产生的
10.2 Linux的中断处理程序架构
- 中断分层
- 顶半部
- 简单地读取寄存器中中断状态,并处理中断标志后就进行“登记中断”的工作
- “登记中断”:将底半部放在该设备的底半部执行队列中,以加快顶半部的执行速度
- 简单地读取寄存器中中断状态,并处理中断标志后就进行“登记中断”的工作
- 底半部
- 处理中断程序的所有事情,可被中断打断
- 顶半部
- 中断要处理的任务较少时可以所有的处理任务放到顶部处理
- cat /proc/interrupts
- 获得中断的统计信息,并且能统计出每个中断号发生的次数
10.3 Linux中断编程
10.3.1 申请和释放中断
1、申请irq
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
-
request_irq()函数
- 参数
- irq:申请的硬件中断号
- handler:向系统登记的中断处理函数,是一个回调函数
- irqflags:中断处理属性
- 中断触发方式
- IRQ_TRIGGER_RISING
- IRQF_TRIGGER_FALLING
- IRQF_TRIGGER_HIGH
- IRQF_TRIGGER_LOW
- 中断处理方式
- IRQF_shared:多个设备共享中断
- 中断触发方式
- dev:传递给中断服务程序的私有数据,一般为设备结构体或者NULL
- 返回值
- 成功:返回0
- 失败:
- 返回-EINVAL:中断号无效或者中断处理函数为NULL
- 返回-EBUSY:中断已经被占用且不能共享
- 参数
int devm_irq_irq(struct devcice* dev, unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id);
-
devm_request_irq()函数
- 与request_irq()函数区别:devm_开头的API申请的是内核“managed”的资源,一般不需要在出错的地方处理和在remove()接口里面在显示的释放
10.3.2 使能和屏蔽中断
- 屏蔽一个中断源
- disable_irq_nosync():立即返回
- disable_irq():等待目前的中断处理完成
- 因为desable_irq()函数会等待指定的中断被处理完成,因此如果在n号中断的顶半部调用disable_irq(n),会引起系统的死锁,这种情况下只能调用disable_irq_nosync(n)
void disable_irq_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);
- 屏蔽本CPU上的全部中断
- local_irq_save()函数:会将中断的状态保留在flags中
- flags为unsigned long类型,被直接传递,而不是通过指针
- local_irq()函数直接禁止中断而不保存状态
- local_irq_save()函数:会将中断的状态保留在flags中
#define local_irq_save(flag) ...
void local_irq_disable(void);
- 恢复本CPU上的全部中断
#define local_irq-restore(flags) ...
void local_irq_enable(void);
10.3.3 底半部机制
Linux实现底半部机制主要有tasklet、工作队列、软中断和线程化irq
-
tasklet
- tasklet执行的上下文是软中断,执行的时机是顶半部返回的时候
- 实际的操作是:定义tasklet及其处理函数,并将两者关联
- DECLARE_TASKLET:实现了定义名称为my_tasklet的tasklet,并将其与my_tasklet_func()这个函数绑定,而传入这个函数的参数为data
- 在调度tasklet的时候引用一个tasklet_schedule()函数
void my_tasklet_func(unsigned long);//定义一个处理函数
//定义一个tasklet结构my_tasklet, 与my_tasklet_func(data)函数相关联
DECLARE_TASKLET(my_tasklet, my_tasklet_func, data);
tasklet_schedule(&my_tasklet);
-
工作队列
- 使用方法和tasklet相似,如下:
struct work_struct my_wq; //定义一个工作队列
void my_wq_func(unsigned long); //定义一个处理函数
INIT_WORK(&my_wq, (void (*)(void *))my_wq_func, NULL); //初始化工作队列并将其与处理函数绑定
schedule_work(&my_irq);//系统在适当的时候需要调度时使用运行
-
软中断
- 使用软件方式模拟硬件中断的概念,实现宏观上的异步执行效果
- 软中断和tasklet仍然运行与中断上下文,而工作队列则运行于进程上下文,因此软中断和tasklet的处理函数不能休眠,但工作队列是可以的。
- softirq_action结构体表征一个软中断
- 这个结构体中包含软中断处理函数指针和传递给函数的参数
- open_softirq()可以注册软中断对应的处理函数
- raise_softirq()函数可以触发一个中断
- local_bh_disable()和local_bh_enable()是内核用于禁止和使能软中断和tasklet底半部机制的函数。
-
threaded_irq
- 内核申请中断的新函数
- request_threaded_irq();
- devm_request_threaded_irq();
- 新的函数比request_irq()函数和devm_request_irq()函数多了个thread_fn参数
- 这两个函数申请中断的时候,内核会为相应的中断号分配一个对应的内核线程,这个线程指针对这个中断号
- 这两个函数支持在irqflags中设置IRQF_ONESHOT标志,这样内核会自动在中断上下文中屏蔽对应的中断号,而在内核调度thread_fn执行后,重新是能该中断号
- 参数
- handler对应的函数执行于中断上下文
- thread_fn参数对应的函数执行于内核线程
- 如果handler结束的时候,返回值是IRQ_WAKE_THREAD,内核会调度线程执行thread_fn对应的函数
- handler参数可以设置为NULL,此时内核会默认的irq_default_primary_handler()代替handler,并会使用IRQF_ONESHOT标志
- 内核申请中断的新函数
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, unsignd long irqflags, const char *devname, void *dev_id);
static irqreturn_t irq_default_primary_handler(int irq, void *dev_id)
{
return IRQ_WAKE_THREAD;
}
10.4 中断共享
- 共享中断的多个设备申请中断时,都应该使用IRQF_SHARED标志,申请成功的前提:
- 该中断未被申请
- 该中断虽然被申请了,但是之前申请的该中断的所有设备也都以IRQF_SHARED标志申请该中断
- 尽管内核模块可以访问的全局地址都可以作为request_irq(…,void *dev_id)的最后一个参数dev_id,但是设备结构体指针显示可传入的最佳参数
- 中断到来时,会遍历执行共享此中断的所有中断处理程序,直到某个函数返回IRQ_HANDLED
- 在中断处理程序顶半部中,应根据硬件寄存器中的信息比照传入的dev_id参数迅速判断是否为本设备的中断,若不是,应迅速返回IRQ_NONE
10.5 内核定时器
10.5.1 内核定时器编程
1、timer_list结构体
- 内核定时器的数据结构
- 定时器期满后,function()成员将被执行
- data成员是其传入的参数
- expires成员是定时器的到期时间(jiffies)
- 定时器期满后,function()成员将被执行
struct timer_list {
struct list_head entry;
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
struct tvec_base *base;
/* ... */
};
- 定义一个名为my_timer的定时器
struct timer_list my_timer;
2、初始化定时器
- init_timer是一个宏,用来初始化timer_list的entry的next为NULL,并给base指针赋值
void init_timer(struct timer_list *timer);
- TIMER_INITALIZER(_function,_expires, _data)宏用于赋值定时器结构体的function、expires、data和base成员
- DEFINE_TIMER(_name,_function,_expires,_data)宏是定义并初始化定时器成员的“快捷方式”
- setup_timer()也可以用于初始化定时器并赋值其成员
3、增加定时器
- 注册内核定时器,将定时器加入到内核的动态定时器链表中
void add_timer(struct timer_list *timer);
4、删除定时器
- del_timer_sync()是del_timer的同步版,在删除一个定时器时需要等待其被处理完,因此该函数的调用不能发生在中断上下文中
int del_timer(struct timer_list *timer);
5、修改定时器
- mod_timer()函数修改定时器的到期时间,在新的被传入的expires到来后才会执行定时器函数
int mod_timer(struct timer_list *timer, unsigned long expires);
6、定时器的使用流程
-----------使用定时器的步骤--------------
struct timer_list my_timer_list;//定义一个定时器,可以把它放在你的设备结构中 struct{定义一个定时器}
init_timer(&my_timer_list);//初始化一个定时器
my_timer_list.expire=jiffies+HZ;//定时器1s后运行服务程序
my_timer_list.function=timer_function;//定时器服务函数
add_timer(&my_timer_list);//添加定时器
void timer_function(unsigned long)//写定时器服务函数
del_timer(&my_timer_list);//当定时器不再需要时删除定时器
del_timer_sync(&my_timer_list);//基本和del_timer一样,比较适合在多核处理器使用,一般推荐使用del_timer_sync
10.6 内核延时
10.6.1 短延时
- 内核提供的纳秒、微妙和毫秒的延时
- 忙等待,根据CPU的频率进行一定次数的循环
- 毫秒延时对内核来说是很大的,最好不要直接用,可以用替代性函数
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
//毫秒延时的替代性函数
vooid msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds);
10.6.2 长延时
- 比较当前的jiffies和目标jiffies,直到未来的jiffies到达目标jiffies
- time_before()与time_after()函数:将传入的未来时间jiffies和被调用时的jiffies进行一个简单的比较
- 为了防止在比较过程中编译器对jiffies的优化,内核将其定义为volatile变量,这将保证每次都会重新读取这个变量
//延迟100个jiffies
unsigned long delay = jiffies +100;
while(time_before(jiffies, delay));
//在延迟2s
unsigned long delay = jiffies + 2*Hz;
while(time_before(jiffies, delay));
10.6.3 睡着延时
- 睡着延时:是在等待的时间到来之前进程处于睡眠状态,CPU资源被其他的进程使用
- schedule_timeout()可以使当前任务休眠至指定的jiffies之后再重新被调用执行
- schedule_timeout的实现原理是向系统添加一个定时器,在定时器处理函数中唤醒与参数对应的进程
- 下面函数可以将当前进程添加到等待队列中,从而在等待队列上睡眠,当超时发生时,进程将它们唤醒
sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);
interruptible_sleep_on_timeout(wait_queue_head_t *q, unsignd long timeout);