Linux驱动学习——中断、定时器

时间:2021-12-24 23:23:55

中断

Linux内核有中断功能的原因:

外设的处理速度远远慢于CPU,如果不采取中断机制,CPU获取外设数据只能靠轮询,这样会降低处理器的利用率。

 

中断源----->中断控制器----->CPU

                        (硬件)

ARM处理器的中断控制器集成在CPU内部,外设和CPU之间的中断信号要靠中断控制器来控制,中断控制器可以管理中断,屏蔽、使能中断、设置优先级等。

 

中断处理流程

CPU响应中断请求后,保护现场(压栈),跳转至异常向量表。Linux内核在初始化时建立异常向量表,向量表的起始地址在0xFFFF0000,异常向量表的每一个处理函数的入口,都有保存现场、恢复现场的代码(内核已经实现)。中断处理函数返回后,恢复现场。

 

中断编程

中断申请:

       intrequest_irq (unsigned int irq, irq_handler_t handler,unsigned long irqflags,

                                                        constchar *name, void *dev_id);

irq:中断号,Linux内核每一个硬件中断都有对应的中断号,对于ARM架构,0 ~ 31号内核保留,所以每个硬件对应的中断号从32开始。

irqflags:中断标志

       IRQF_SHARED:表示多个设备共享终端。

       IRQF_SAMPLE_RANDOM:用于随机数种子的随机采样。

       IRQF_TRIGGER_RISING/ IRQF_TRIGGER_FALLING

IRQF_TRIGGER_HIGH   / IRQF_TRIGGER_LOW:

对于内部中断irqflags一般指定0。

name:中断名称,申请成功后可通过cat /proc/interrupts 查看中断的注册信息。

 

irqretrun_t (*irq_handler_t) (int irq ,void *dev_id);

中断处理成功返回IRQ_HANDLED;如果无需处理返回IRQ_NONE。

中断处理函数的要求:

1.       中断处理函数不隶属于任何进程,不参与进程的调度和切换。

2.       中断处理函数不能直接和用户空间进行数据交互,如果要进行数据交互,要配合系统调用。

3.       内核为中断分配的栈只有1页。

4.       中断处理函数中不能调用可能引起阻塞的函数。

(copy_from_user/ copy_to_user / kmalloc)

5.       中断处理函数的执行速度越快越好。

 

共享中断:

多个设备共享一根中断线。有多个设备驱动,就代表要多次进行request_irq,那么一个中断号就会有多个中断处理函数。每当CPU响应这个中断时,CPU依次执行每一个中断处理函数,在中断处理函数中判断这个信号是否是这个设备产生的,如果是,就处理中断,如果不是,直接返回IRQ_NONE.共享中断要求设备本身具备判断中断的功能。在每一个设备驱动在申请中断时,都要指定IRQF_SHARED。在申请中断时,必须给中断处理函数传递唯一的参数。这个参数是给free_irq使用。要指定释放的是哪个中断处理函数。在中断处理函数中,不能调用disable_irq

 

Linux内核中断处理程序结构:顶半部和底半部

内核要求中断处理函数执行的速度越快越好。某些场合中断处理函数执行的时间会很长势必会影响系统的并发能力和相应能力。所以把耗时的工作从中断处理程序中分离出来放在底半部执行。

 

linux内核底半部的实现机制:

1.tasklet

内核描述tasklet使用的数据结构:

    struct tasklet_struct {

       //底半部的处理函数,里面做中断其余的事情,CPU在适当的时候执行此函数。

       void (*func)(unsigned long);

       //给底半部处理函数传递的参数,一般data保存指针

       unsigned long data;

    };

 

1)       分配初始化tasklet

      DECLARE_TASKLET(taskletname, func, data);

       或者

       structtasklet_struct mytask;

      tasklet_init(&mytask, func, data);

       fun:底半部的处理函数,需要自己去实现,里面做比较耗时的内容。

       data:给处理函数传递的参数。

2)       在顶半部(中断处理函数中)调用tasklet_schedule(&mytask)进行登记(切记不是函数的调用)一旦完成登记,顶半部立即执行完毕,CPU会在适当的时候执行底半部的处理函数。

3)       注意事项:tasklet工作在中断上下文中!所以实现的要求也要遵循中断处理函数的要求。

 

2.工作队列

由于tasklet还是工作在中断上下文中,所以不能进行休眠操作。但是某些场合如果想在底半部进行休眠操作,这时采用工作队列即可。工作队列工作在进程上下文,允许参与进程的调度。

linux内核描述工作队列使用相关的数据结构:

structwork_struct ; //工作

structdelayed_work;//延时工作

 

1)       分配工作或者延时工作

structwork_struct mywork;

structdelayed_work mydwork;

 

2)       初始化

INIT_WORK(&mywork,mywork_func); //这个函数会在CPU空闲时被执行

INIT_DELAYED_WORK(&mydwork,mydwork_func); //这个函数的执行可以指定时间

3)       在顶半部的中断处理函数中进行登记工作或者延时工作:

schedule_work(&mywork); //一旦完成登记,CPU在适当的时候执行工作的处理函数

schedule_delayed_work(&mydwork,5*HZ); //一旦完成登记,CPU会在5秒以后执行延时的工作处理函数。如果5秒还没有到期,有重新登记,会使得上次的登记无效,重新开始。

这两个函数是将工作或者延时工作交给内核默认的线程和工作队列,有时可以创建自己的工作队列和内核线程:

create_workqueue //给每一个CPU都会创建

create_singlethread_workqueue //创建一个内核线程和工作队列,这个内核线程和工作队列不会和具体的某一个CPU进行绑定。

queue_work

queue_delayed_work

4)       注意:工作队列工作在进程上下文中

 

软中断:

tasklet也是基于软中断实现。软中断本质也是延后执行的函数!软中断也会涉及相关的延后执行函数。

相同点:

tasklet和软中断都是工作在中断上下文中;

不同点:

同一类tasklet只能在一个CPU核上执行,但是软中断涉及的延后函数可以同时在多个CPU上执行。既然软中断的处理函数能在多个CPU上同时执行,所以这个函数在设计的时候必须要求具有可重入性!并且软中断的实现代码必须静态编译到内核中,不能以模块形式加载!

函数的可重入性的要求:

1.不允许在函数体内访问全局变量

2.如果要访问全局变量,需要进行互斥访问,结果是函数的执行效率变低

 

linux内核定时器:

系统的时间管理者靠系统定时器硬件。

系统定时器硬件特点:

1.能够通过软件来配置它的工作频率

2.一旦设置好工作频率,启动硬件定时器,定时器就会以一定的频率向CPU发送中断信号。

3.linux内核默认有对应的定时器硬件的中断处理函数:

  3.1更新系统的运行时间

  3.2更行系统的实际时间

  3.3计算每一个进程的时间片,然后决定是否需要调度

  3.4如果有超时的定时器,处理超时的定时器

  ...

4.定时器硬件源源不断以一定的频率向CPU发送中断,linux内核对应的中断处理函数就会以一定的频率一直被调用。

 

5.HZ:与体系结构相关,ARM架构HZ=100,表明硬件定时器1秒钟会产生100次的时钟中断。每一次的时钟中断的间隔10ms,这个时间也是定时器硬件能处理的最小时间。如果小于10ms的时间,不能再使用定时器硬件,而是使用CPU的工作频率和CPU处理一条指令的时间来换算。

 

6.jiffies:它是内核的全局变量,记录自开机以来发生了多少次时钟中断。如果HZ=100,每10ms,jiffies就会在时钟中断的处理函数中加1.一般内核使用jiffies来表示当前时间。

注意:jiffies回绕问题,内核通过time_after/time_before来解决回绕问题

 

6.内核定时器

数据类型,数据结构:

struct timer_list {

       unsignedlong expires; //超时时候jiffies的值

       例如:expires =jiffies + 5*HZ

       void(*function)(unsigned long) //超时处理函数,一旦定时器的超时时间到期,内核执行这个处理函数

       unsignedlong data;//给超时处理函,数传递的参数,一般内核传递指针

};

定义一个自己的定时器

1.分配定时器

   struct timer_list mytimer;

2.初始化定时器

  init_timer(&mytimer); //这个函数内核仅仅初始化它关心的字段。

  指定其他相关的字段:

  mytimer.expires = jiffies + 5*HZ; //超时时间间隔5秒

  mytimer.function = mytimer_function; //指定超时处理函数

 mytimer.data = (unsigned long)&mydata;//给超时处理函数传递的参数

 

3.将定时器添加到内核之中。

  add_timer(&mytimer); //一旦完成添加,内核就开始管理这个定时器,开始计时!一旦超时,内核执行超时处理函数,内核同时将定时器删除掉。注意超时处理函数只执行一次。

超时处理函数最后重新添加定时器重复执行。如果添加如下代码:

mytimer.expires = jiffies + 5*HZ;

add_timer(&mytimer);

如果添加以上代码,由于linux内核支持任务的抢占,中断和SMP,会有并发和竞态问题,导致以上代码执行的结果具有不确定性。以上两个操作步骤不是原子操作!

 

4.修改定时器

  mod_timer(&mytimer, jiffies + 3*HZ);

  mod_timer等价于一下三个步骤:

  1.删除定时器del_timer(&mytimer);

  2.修改定时器的超时时间

     mytimer.expires = jiffies + 3*HZ

  3.添加定时器

     add_timer(&mytimer);

  这个函数是原子的!所以如果向让超时处理函数重复执行,可以在最后添加mod_timer重新添加定时器即可。

 

5.删除定时器del_timer(&mytimer);

  如果定时器到期,内核帮你删除

  定时器没有到期,可以调用此函数提前将定时器删除

 

将ms转成对应的jiffies

msecs_to_jiffies(毫秒数);

 

linux内核短延时函数:

ndelay/udelay/mdelay:这个函数会让CPU忙等待

如果延时时间>1ms,就考虑使用mdelay

如果延时时间>11ms,就考虑使用硬件定时器的相关延时

 

读取CPU的信息: cat/proc/cpuinfo