4、中断与定时器的学习
一、中断
所谓的中断,是指 CPU 在执行的过程中,出现了某些的突发时间,CPU 必须暂停当前程序的执行,转而去处理突发的事件,当处理完毕之后,又返回源程序继续执行。
1.1、中断的分类
按照中断的来源: 可以分为内部和外部的中。外部中断,也就是由外设请求的中断;内部中断,显示就是 CPU 请求的中断,比如软中断,除法错误。
按照总段屏蔽的情况:分为可以屏蔽和不可屏蔽。
1.2、中断的常用接口
1、中断的申请
request_irq(unsigned int irq,irq_handler_t handler,unsigned long flags,const char * devname,void * dev_id)
当完成 request_irq 的时候,内部其实已经是调用了 enable_irq 了,就是中断不仅注册而且使能了,后面已经不需要再进行 enable_irq
irq : 申请的中断号
handler : 登记的中断处理函数,函数指着来着
flags : 中断标记(属性标记),指定中断的类型,当为 SA_INTERRUPT : 快速中断,IRQF_SHARED : 共享中断,也就是中断的管脚是被多个管脚共享的,
devname : 名字,这个名字,当中断注册成功之后,就会在 /proc/interrupt 里面显示,cat 的时候,就可以看到,
dev_id : 私有的数据,一般是设置为零,当中断设置为共享中断的时候,就可以设置传输的数据,一般传输数据是结构体
当注册玩中断的时候,可以查看 注册进去的中断,
cat /proc/interrupts
CPU0 CPU1 CPU2 CPU3
1: 0 0 0 0 Phys-irq i8042
8: 3 0 0 0 Phys-irq rtc
9: 0 0 0 0 Phys-irq acpi第一列是中断号,而最后一列,就是我们注册进去的 dev_name
2、中断的释放
free_irq(unsigned int irq,void * dev_id)
irq : 释放的中断号
dev_id : 中断传输的数据,
3、禁止单个中断
void disable_irq(int irq);
void disable_irq_nosync(int irq);
void enable_irq(int irq);
enable 与 disable 是相呼应的,而disable 关闭中断的是,一般是会等到中断处理完毕在之后才返回,而 nosync 则是立即返回。因为不反悔的话有可能中断的清理工作 N 久,导致了 死锁的产生。
4、禁止所有的中断
实现屏蔽本 CPU 内,所有的中断
#define local_irq_save(flags)
void local_irq_disable(void)
前者的宏代码,会在屏蔽中断的同时,将中断状态保留在 flags 中,而 后面的则不会。
要想将中断回复的话,就要用下面的代码:
#define local_irq_restore(flags) // 之前保留的状态有用了
void local_irq_enable(void)
1.3、中断的机制
在产生中断以后,中断要去执行长时间的任务;而中断的数量又是有限的,要实现满足大量中断的功能,又要实现快速的反映,Linux 使用了将中断分为上半部和下半部的机制
上半部,实现的内核的注册之后,中断发生之后的快速反映部分,其实就是 request_irq 指定的中断函数。中断发生,立马跳转到中断函数里面执行,快速的反映。而后半部分的话,则是在前半部分里面调调度延迟处理的机制,对那些耗时的工作,放在下半部处理。,用延迟处理的函数,来实现。而上部分调度延迟处理的机制,一般是 tasklet 机制 和工作队列机。
1.3.1、tasklet 机制
tasklet 机制,实质上,重新指定了一个新的函数,让这个新的函数,在适当的实际去运行,也就是在下半部运行,而上半部的中断就可以释放了,这样上半部空出来资源,就得到释放。
void my_tesklet_for_func(unsigned long data) // 函数
DECLARE_TASKLET(my_tesklet, my_tesklet_for_func,data); // 初始化
实现了,将一个执行函数,绑定了 tasklet 机制上面,而第一个参数是名字,随意都可以的,而 data 则是传输给 指定函数的值。
完成了函数的绑定,tasklet 机制上运行了下半部,但是必须在上半部分,也就是 request_irq 指定的函数里面,进行调用,
tasklet_schedule(&my_tesklet)
这里需要知道的是,tasklet 机制,函数的绑定,在函数外就可以被执行,tasklet 的调度运行,则是在上半部被指定,这样才可以跳转到下半部分被得到运行嘛。
注意:
在单核的情况下,tasklet 机制,不用加锁;而在多核的情况下, tsaklet 可以保证函数都是运行在第一个调度它们的 CPU 上,因此一个中断处理,可以确保一个 tasklet 在处理结束前不会被执行。但是,另一个中断是存在可能在 tasklet 在运行时被提交,因此,tasklet 和中断之间还是存在需要加锁的需要。
1.3.2、工作队列
工作队列的与 tasklet 类似,但是工作队列的执行上下文都是在内核线程,因此工作队列是可以被调度和睡眠,显然 tasklet 是不能睡眠的。
工作队列比 tasklet 多了一个 struct work_struct 结构体,这个结构体就是专门的过队列
struct work_struct { atomic_long_t data; #define WORK_STRUCT_PENDING 0 /* T if work item pending execution */ #define WORK_STRUCT_STATIC 1 /* static initializer (debugobjects) */ #define WORK_STRUCT_FLAG_MASK (3UL) #define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK) struct list_head entry; work_func_t func; #ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map; #endif };这里主要是
work_func_t 这个宏定义,其实就是函数指针。struct work_struct my_work_struct;
void my_func_for_work(struct work_struct *work);
上面定义了工作队列和工作队列的函数。
需要初始化工作工作队列,
INIT_WORK(&my_work_struct,my_func_for_work);
将工作队列与函数进行绑定,
完成绑定只是前奏,需要在上半部,也就是 request_irq 指定的函数里面,调用工作队列:
schedule_work(&my_work_struct);
二、定时器
之所以将定时器与中断放在一个章节,是因为定时器的实在在硬件也是依赖中断实现的,系统走动的的每一个节拍都是被记录在 jiffies 上。对比两个时间点的 jiffies 就可以得到这段时间走过的节拍数目,也就可以得到时间。
2.1、定时API实现
1、timer_list 结构体
struct timer_list{
struct list_head entry; //内核使用
unsigned long expires; //设定的超时的值
void(*function)(unsigned long); //超时处理函数
unsigned long data; //超时处理函数参数
struct tvec_base *base; //内核使用
}expires : 设定定时器的时间
function : 函数指针,当定时器时间到了,就会调用者这个函数
data : 传参,当function 被指定的时候,这个 data 就会传给 function 函数。
2、定时器的初始化
init_timer(struct timer_list * timer)
参数为定义好的定时器 timer_list 结构体
3、定时器增加
void add_timer(struct timer_list *timer)
定时器的增加。,其实就是将定时器假如到内核的定时器链表中,假如之后,才是真正开始计时。当定时器的时间到达的时候,就会去执行 function 函数指针指定的函数,
4、删除计时器
int del_timer(struct timer_list *timer);
删除定时器,当然还存在同步的版本: del_timer_sync、。
5、定时器的修改
int mod_timer(struct timer_list *timer,unsigned long expires)
修改定时器的延迟时间,被修改之后,会重新开始计时。
2.2、内核的延迟
1、短延迟
内核提供了下面三个函数进行短延迟,分别是纳秒、微秒、毫秒延迟:
void ndelay(unsigned long nsecs)
void udelay(unsigned long usecs)
void mdelay(unsigned long msecs)
延迟,是让 CPU 进入了忙等待,这个时候, CPU 都给浪费了,所以不建议使用较长时间的延迟,优点是,这个时候这个延迟的时间是准的。
内核也提供较长时间按的睡眠机制 ,使得 CPU 在等待的时间,可以去执行其他的进程:
void msleep(unsigned int millisecs); // 睡眠
unsigned long msllep_interruptible(unsigned int millisecs) //可以被中断的睡眠
void ssleep(unsigned int seconds); // 秒 睡眠
2、长时间延迟
长时间的延迟,可以通过指定的时间节拍与 jiffies 进行对比。
unsigned long delay = jiffies + 100; // 延迟 100 个节拍;
while(time_before(jiffies,delay));
delay = jiffies + 2 * HZ; // 延迟两秒,HZ 是一秒钟的节拍
while(time_before(jiffies,delay));
#define time_before(a,b) time_after(b,a)
#define time_after(a,b) \
(typecheck(unsigned long, a) && \
typecheck(unsigned long, b) && \
((long)(b) - (long)(a) < 0))
3、等待队列
等待队列,一般是用在中断、定时器、进程同步下。一般是,当进程必须在等到(类似阻塞)某种事情发生,才进行后续的工作。阻塞的过程,是睡眠的过程,当等待的条件为真(资源获取到),就由内核唤醒进程。
等待队列,是由一个“队列头”进行管理的,
1、队列头初始化
static DECLARE_WAIT_QUEUE_HEAD(queus_name);
queus_name ,是队列的头名字,随意设置,
2、进入睡眠
进程在还没有等到某种条件,或者资源的时候,就进入睡眠的状态,
wait_event(queus_name , condition) // 不可中断
wait_event_interruptible(queus_name , condition) // 可以被中断
queus_name : 是等待队列头的名字,
condition : 是一个值,一般是 bool 类型的,必须满足 condition 为真,否则继续睡眠
3、唤醒
进入睡眠状态的进行,需要在一定的位置进行唤醒,
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);
中断与不可中断不同的 API ,要看 进入睡眠的时候,使用哪个 API ,