第十章 中断与时钟

10.1 中断与定时器

  • 中断的划分
    1. 来源:中断分为内部中断(CPU内部)和外部中断(外设提出的请求)
    2. 是否可屏蔽:可屏蔽中断与不可屏蔽中断(NMI)
    3. 入口跳转方法:中断可分为向量中断和非向量中断(向量中断由硬件提供中断服务程序入口地址,非向量中断由软件提供中断服务程序入口地址)
      1. 向量中断:CPU通常为不同的中断分配不同的中断号,当检测到某中断号的中断到来后,就自动跳转到与该中断号对应的地址执行。
      2. 非向量中断:非向量中断的多个中断共享一个入口地址,进入该入口地址后,再通过软件判断中断标志来识别具体是哪个中断。
  • ARM中中断控制器为GIC,支持三种类型中断:
    1. SGI(Software Generated Interrupt):软件产生的中断,可以用于多核的核间通信,一个CPU可以通过写GIC的寄存器给另外一个CPU产生中断。
    2. PPI(Private Peripheral Interrupt):某个CPU私有外设的中断,这类外设的中断只能发给绑定的那个CPU。
    3. SPI(Shared Peripheral Interrupt):共享外设的中断,这类外设的中断可以路由到任何一个CPU。
  • 对于SPI类型的中断,内核可以通过如下API设定中断触发的CPU核:
    extern int irq_set_affinity (unsigned int irq, const struct cpumask *m);

10.2 Linux中断处理程序架构

  • Linux将中断处理程序分解为两个半部:顶半部(Top Half)和底半部(Bottom Half)
    1. 顶半部完成尽量少的比较紧急的功能(一般读取寄存器中的中断状态,并在清除中断标志后就进行“登记中断”的工作)
    2. 底半部来完成中断事件的绝大多数任务。而且可以被新的中断打断。

  • 通过查看 /proc/interrupts文件可以获得系统中断的统计信息,可以统计每一个中断号上的中断在每个CPU上发生的次数

10.3 Linux中断编程

10.3.1 申请和释放中断

  • 申请和释放中断:
    1. 申请irq

      申请中断

      1

      int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)

      irq: 要申请的硬件中断号
      handler:中断处理函数(顶半部)
      irqflags:中断标志,在文件 include/linux/interrupt.h 里面,常用的中断处理标志:

      name:中断名字
      dev: 如果将 flags 设置为 IRQF_SHARED 的话,dev 用来区分不同的中断,一般情况下将 dev 设置为设备结构体,dev 会传递给中断处理函数 irq_handler_t 的第二个参数。
      返回值:返回0成功,-EINVAL表示中断号无效或处理函数指针为NULL,-EBUSY表示中断已经被占用且不能共享

    2. 中断处理函数(顶半部):

      顶半部中断处理函数

      1

      irqreturn_t (*irq_handler_t)(intvoid *)

      第一个参数为中断号,第二个参数和 request_irq() 函数中的dev参数相同

    3. 释放irq:

      1

      void free_irq(unsigned int irq, void *dev);

      参数同request_irq()函数。

10.3.2 使能和屏蔽中断

  • 中断使能和禁止(单个中断):
    1. 中断使能:

      void enable_irq(unsigned int irq)

    2. 中断禁止:
      1. void disable_irq(unsigned int irq)

        等中断处理完成才返回

      2. void disable_irq_nosync(unsigned int irq)

        立即返回

    3. 不能在中断上半部调用  disabled_irq()  函数,由于会等待中断处理完毕,因此会形成死锁。此时只能调用  disable_irq_nosync()
    4. 全局中断使能和关闭(只对本CPU有效):
      1. local_irq_enable()
      2. void local_irq_disable(void)
    5. 全局中断保存和恢复(仅对本CPU有效):
      1. 禁用并中断保存:local_irq_save(flags)
      2. 中断恢复:local_irq_restore(flags)

10.3.3 底半部机制

  • Linux实现底半部的机制主要有tasklet、工作队列、软中断和线程化irq。
  • tasklet
    1. 执行上下文是软中断,执行时机一般是顶半部返回的时候
    2. 首先定义一个tasklet的处理函数,然后通过宏DECLEAR_TASKLET(my_tasklet, my_tasklet_func, data)将tasklet和其处理函数绑定,最后在需要调度时使用tasklet_schedule(&my_tasklet)函数。
    3. tasklet使用模板:

  • 工作队列
    1. 执行上下文是内核线程,因此可以调度和睡眠
    2. 定义工作队列和处理函数:
    3. 初始化工作队列:
    4. 工作队列使用方法:
  • 软中断
    1. 执行时机是顶半部返回的时候,tasklet是基于软中断实现的
    2. 使用结构体softirq_action表示软中断(成员变量为中断处理函数指针)
    3. 使用open_softirq()函数进行注册软中断处理函数,使用raise_softirq()触发软中断
    4. 禁止和使能软中断和tasklet:local_bh_disable()和local_bh_enable()
    5. 硬中断、软中断、信号的区别:
      1. 硬中断是外部设备对CPU的中断
      2. 软中断是中断底半部的一种处理机制(和软件指令引发的中断不同,比如通过软中断陷入ARM内核)
      3. 信号是内核(或其他进程)对某个进程的中断,例如异步通知
  • threaded_irq
    1. 内核可以通过以下函数代替request_irq()函数进行中断申请
    2. 该函数比request_irq()多一个参数thread_fn,该函数会为相应的中断号分配一个内核线程,该线程只针对这个中断号
    3. 参数handler对应的函数执行于中断上下文,thread_fn参数对应的函数则执行于内核线程。如果handler结束的时候,返回值是IRQ_WAKE_THREAD,内核会调度对应线程执行thread_fn对应的函数。
    4. 支持在irqflags中设置IRQF_ONESHOT标记,这样内核会自动帮助我们在中断上下文中屏蔽对应的中断号,而在内核调度thread_fn执行后,重新使能该中断号。
    5. handler参数可以设置为NULL,这种情况下,内核会用默认的irq_default_primary_handler()代替handler,并会使用IRQF_ONESHOT标记。 

10.4 中断共享

  • 使用方法:
    1. 申请中断时,应该使用IRQF_SHARED
    2. 申请中断时,最后一个参数传入设备结构体
    3. 中断到来时,会遍历执行共享中断的所有中断处理程序,知道某个函数返回IRQ_HANDLED,在顶半部中会根据硬件寄存器中的信息和dev参数进行判断是否为本地设备的中断,若不是,返回IRQ_NOTE。

  • 编程要点

10.5 内核定时器

  • 使用timer_list定义一个定时器:(重点成员变量:expires、function、data)

    定义一个my_timer的定时器

    1

    struct timer_list my_timer;

  • 初始化timer:

    1

    void init_timer(struct timer_list *timer)

  • 使用宏对定时器成员变量赋值(也可像结构体一样赋值)

    TIMER_INITIALIZER(_function, _expires, _data)

  • 下宏可以定义并初始化定时器

    DEFINE_TIMER(_name, _function, _expires, _data)

  • 增加定时器、删除定时器、修改定时器的expire

    1

    2

    3

    void add_timer(struct timer_list *timer);

    int del_timer(struct timer_list *timer);

    int mod_timer(struct timer_list *timer, unsigned long expires);

    1. 删除定时器时需要等待其处理完,因此del_timer()不能发生在中断上下文

    2. mod_timer用于修改定时器到期时间,新传入expires到来后才会执行定时器函数
    3. 如果修改时定时器还没有激活的话,mod_timer 函数会激活定时器
  • 定时器使用模板

    /* xxx设备结构体 */

     struct xxx_dev {

     3  struct cdev cdev;

     4  ...

     5  timer_list xxx_timer;      /* 设备要使用的定时器 */

     6 };

     7

     /* xxx驱动中的某函数 */

     9 xxx_func1(…)

    10 {

    11  struct xxx_dev *dev = filp->private_data;

    12  ...

    13  /* 初始化定时器 */

    14  init_timer(&dev->xxx_timer);

    15  dev->xxx_timer.function = &xxx_do_timer;

    16  dev->xxx_timer.data = (unsigned long)dev;

    17                      /* 设备结构体指针作为定时器处理函数参数 */

    18  dev->xxx_timer.expires = jiffies + delay;

    19  /* 添加(注册)定时器 */

    20  add_timer(&dev->xxx_timer);

    21  ...

    22 }

    23

    24 /* xxx驱动中的某函数 */

    25 xxx_func2(…)

    26 {

    27 ...

    28  /* 删除定时器 */

    29  del_timer (&dev->xxx_timer);

    30  ...

    31 }

    32

    33 /* 定时器处理函数 */

    34 static void xxx_do_timer(unsigned long arg)

    35 {

    36  struct xxx_device *dev = (struct xxx_device *)(arg);

    37  ...

    38  /* 调度定时器再执行 */

    39  dev->xxx_timer.expires = jiffies + delay;

    40  add_timer(&dev->xxx_timer);

    41  ...

    42 } 

  • 调度一个delayed_work在指定的延时后执行:

    int schedule_delayed_work(struct delayed_work *work, unsigned long delay)

  • 取消delayed_work

    1

    2

    int cancel_delayed_work(struct delayed_work *work)

    int cancel_delayed_work_sync(struct delayed_work *work)

  • 如果需要周期性执行任务,需要在delayed_work的工作函数中再次调用schedule_delayed_work()

10.6 内核延时

  • 内核中的三个延时函数(忙等待类型):

    1

    2

    3

    void ndelay(unsigned long nsecs);   //纳秒

    void udelay(usnigned long usecs);   //毫秒

    void mdelay(usnigned long msecs);  //微秒

  • 在bootloader传递给内核的bootargs中设置lpj=1327104可以省略校准,节约百毫秒的开机时间

  • 对毫秒级以上的延迟,建议用以下函数

    1

    2

    3

    void msleep(unsigned int millisecs);    /*不可以被打断*/ 

    unsigned long msleep_interruptible(unsigned int millisecs);   /*可以被打断*/

    void ssleep(unsigned int seconds);  /*不可以被打断*/ 

  • 内核中延迟直观的方法是比较当前jiffies和目标jiffies,可通过以下函数实现

    1

    2

    timer_before();

    timer_after(); 

  • 睡着延迟:等待的时间到来之前进程处于睡眠状态,CPU资源被其他进程使用

    schedule_timeout()  /*当前任务休眠至指定的jiffies后重新被调度执行*/

  • 将当前进程添加到等待队列中,从而在等待队列上睡眠,当超时发生时,进程将被唤醒(后者可以在超时前被打断)

    1

    2

    sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout)

    interruptible_sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值