4、中断与定时器的学习

时间:2021-02-20 00:13:09

一、中断

    所谓的中断,是指 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 使用了将中断分为上半部下半部的机制4、中断与定时器的学习

    上半部,实现的内核的注册之后,中断发生之后的快速反映部分,其实就是  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 ,